lunes, 16 de julio de 2007

Optimizando código con GCC

Orestes Leal R. [orestesleal13022@cha.jovenclub.cu]

Primeramente un saludo a los lectores habituales de BlackHat, así como los nuevos que se suman a esta positiva iniciativa, pues sobre todo porque potencia el acceso a la libre información de la que "muchos" no disponen a diario, ni siquiera muy a menudo en nuestra Cuba de hoy.

Entre tantos temas posibles sobre los cual redactar en este inmenso mundo de software e implementaciones de GNU/Linux, pensé en varias cosas, sin embargo, la primera por la que me decidí fue por escribir el presente artículo y documentar algo no tan conocido por los usuarios novatos/intermedios de GNU/Linux. Me refiero a la optimización de programas a través de la compilación de los mismos con modificaciones en los "flags" del compilador GCC.

Algunos se preguntarán de qué me sirve y en realidad qué puedo ganar con ello; para dejarlo bien claro todo esto se resume así:

  1. Cuando compilamos un programa tenemos la oportunidad de optimizarlo.
  2. Optimizarlo para que se ajuste a las instrucciones (SSE1, SSE2, SSE3, por poner un ejemplo) presentes en un determinado procesador (ej: Pentium4).
  3. Lograr que el binario (ejecutable nativo de la plataforma) tenga un size "mucho" más reducido, menos consumo de memoria y mayor velocidad.

Con estos principios sobre la mesa vamos a repasar cómo podemos optimizar el software en nuestra distribución GNU/Linux preferida.

[ Flags del compilador ]

Viéndolo de una manera sencilla, los flags son opciones que se le pasan al compilador a modo de parámetros, las cuales él reconoce y toma acciones sobre ella. Lo importante es que al usar los flags de GCC tenemos muchas posibilidades de optimizar, por ejemplo, para uso diario normal o uso diario para desarrollo. Para ello se deben ajustar los flags y generar un binario con símbolos de debugging; esto último por citar un caso.

[ Flags por defecto ]

La mayoría de los programas y bibliotecas son compiladas por defecto en nivel de optimización 2 (un nivel normal) con las opciones -g -O2 del GCC. Hay que tener bien claro que la compilación de programas para Linux se basa en la GNU/ToolChain, unión de gcc/make/autoconf/libtool/automake y algunos más, por lo cual el 100% de los programas común y corrientes al menos incluyen un fichero Makefile (no siempre, pero generalmente el Makefile es generado después del proceso llevado a cabo a través del script configure, para que después el Makefile suela incluir los flags del compilador detectados por defecto, entre otras cosas). En este caso las pruebas las voy a hacer con el código fuente del reproductor Alsaplayer, versión 0.99.78.

Para comprobar lo antes dicho, he descomprimido el file con el código y, tras hacer un cd al dir, compruebo de hecho que el Makefile no existe. Esto ocurre primeramente porque Alsaplayer basa su construcción en una conformación clásica de las herramientas GNU que siguen los siguientes pasos:

  1. Configurar el árbol de las fuentes (configure)
  2. Crear un Makefile (sobre todo con los datos provenientes de makefile.in y makefile.am), con los flags detectados del compilador, linkeador, bibliotecas, etc.
  3. El Makefile generado tendrá una serie de reglas que le indicarán al compilador qué nivel de optimización tendrá el binario o bibliotecas (por defecto recuerden que es 02 en casi todos los paquetes de código fuente), contra qué bibliotecas se van a enlazar, los parámetros del enlazador (LD), reglas de limpieza del árbol, entre muchas otras incluyendo "siempre" los flags del compilador.
  4. Crear todo el código objeto y enlazarlo con "make" (en realidad el enlazado lo realiza el GNU LD, uno de los linkeadores más efectivos en la actualidad.

Esto es una mirada muy básica al proceso; en realidad es mucho más complejo.

[ CFLAGS y CXXFLAGS ]

Estos flags son claves. A través de ellos vamos a indicarle al compilador para qué procesador y el nivel de optimización con que deben ser compilados los sources.

CFLAGS:
Como lo indica, son los flags para el compilador de códigos fuente en lenguaje C.

CXXFLAGS:
Lo mismo, para C++.

Hay varias maneras de establecer los flags de GCC; sin embargo, la más sencilla y útil de todas es a través de variables de entorno, ya que configure las lee si estuvieran establecidas y agrega su valor a la variable CFLAGS en el Makefile final. Otra manera es editar el Makefile generado a mano y modificar las entradas CFLAGS y CXXFLAGS.

Dicho esto y como banco de pruebas, primeramente voy a configurar Alsaplayer sin flags para que detecte solamente lo que esté establecido por defecto. Corro configure de la siguiente manera:

./configure --prefix=/opt/alsaplayer-test

La salida más importante que nos interesa es la siguiente:

"checking for gcc optimization flags... -O2 -fexpensive-optimizations
-funroll-loops -finline-functions -ffast-math -Wall"

En ese momento se chequearon los flags de optimización de GCC, pues como mencionaba anteriormente casi el 100% de los paquetes se ajustan para la optimización en nivel 2, que es la común (todas las distribuciones de Linux basadas en i386 tienen este nivel de optimización para poder correr lo mismo en un 486 que en un Pentium4). Estos flags también se ven afectados por algunos parámetros definidos dentro del propio sistema de construcción de Alsaplayer, por ejemplo -funroll-loop (desenrollado de bucles) o -Wall (muestra todos los warnings o advertencias en el momento de la compilación).

Después finalizado el configure, un detalle importante es que se ha creado el Makefile que GNU make usará para llamar al compilador, enlazador y algunas utilidades necesarias; el detalle es que en la cabecera del nuevo Makefile se encuentra una línea como ésta:

# Makefile. Generated from Makefile.in by configure.

Esto no lleva mucha explicación, porque ya lo he mencionado: se ha creado Makefile desde el Makefile.in y el trabajo lo ha hecho el script shell configure.

Si vemos ahora el contenido de Makefile, podemos hacer una búsqueda de los flags del compilador que se han escrito en él. Exactamente las cadenas en cuestión son:

CFLAGS = -g -O2
CXXFLAGS = -g -O2

Aquí hay definido un nivel de optimización por default.

Seguido, ejecuto un make y make install para compilar y, finalmente, para instalar Alsaplayer en mi sistema y comprobar con estas optimizaciones qué size tiene el binario final, durante la compilación vemos líneas como éstas (se nota el uso de los flags inmediatamente):

"-Wall -g -O2 -D_REENTRANT -DADDON_DIR=\"/opt/alsaplayer-test/lib/alsaplayer\" -g -O2
-MT support.lo -MD -MP -MF .deps/support.Tpo -c -o support.lo support.cpp"

Una vez finalizado, la salida de un:

ls -lh /opt/alsaplayer-test/bin/alsaplayer | awk '{ print $5 }'

Luce así:

"791K"

Un size que no me gusta para nada, tratándose de un software tan pequeño. Dicho esto, vamos a ver cómo ajustamos los flags para generar un binario más reducido.

[ Estableciendo los flags ]

Lo primero es establecer la variable CFLAGS. Vamos allá y en este ejemplo voy a optimizar la generación de código para Pentium4.

export CFLAGS="-s -O3 -march=pentium4"

Aquí el nivel de optimización es el más alto (O3); nótese que no es CERO, sino la letra "O". Otro detalle es que optimizamos para Pentium4 y generamos código dependiente del procesador.

De por sí esto solamente no basta, vamos a setear CXXFLAGS así:

export CXXFLAGS=$CFLAGS

Esto quiere decir que CXXFLAGS va a ser igual al valor actual de la variable de entorno, ya establecida CFLAGS, o sea, las mismas optimizaciones. Vamos a comprobar en el shell:

echo $CFLAGS
echo $CXXFLAGS

La salida debe ser idéntica en los dos casos, ya que CXXFLAGS posee el mismo valor que CFLAGS. Hasta aquí ya estamos listos, para mi ejemplo voy a volver a compilar Alsaplayer con estas variables ya establecidas y a repasar el proceso a ver qué sucede. Para ello limpio el árbol de las fuentes de Alsaplyer con make distclean. Esto borra todos los códigos objeto, binarios, bibliotecas y elimina el Makefile generado, o sea, crea el entorno desde cero, limpio.

Corro el mismo configure de hace un momento:

./configure --prefix=/opt/alsaplayer-test-optimizado

Aquí lo diferente es que lo instalo en otro lugar para después poder ver la diferencia entre el anterior. Terminado el configure paso a analizar el contenido del Makefile generado. La variable CFLAGS luce así:

CFLAGS = -s -O3 -march=pentium4

y CXXFLAGS así:

CXXFLAGS = -s -O3 -march=pentium4

Si comparamos con la anterior podemos ver la diferencia, vuelvo a hacer make y make install y observo esta línea, por ejemplo:

" -s -O3 -march=pentium4 -D_REENTRANT -DADDON_DIR=\"/opt/alsaplayer-test/lib/alsaplayer\""

Donde de hecho, ya se están aplicando los flags.

El momento ha llegado y la salida de un:

ls -lh /opt/alsaplayer-test-optimizado/bin/alsaplayer | awk '{ print $5 }'

Luce así:

"163K"

Es, evidente la gran optimización que se ha producido, desde 791 Kb hasta 163 Kb.

Como ven son muchas las ventajas, pero no todo es así de sencillo; existen aplicaciones que no compilarán bajo estos flags de optimización, ya que sencillamente no están hechas para ellos. Lo que sí me provoca alegría es saber que mientras la versión precompilada de Apache2 me ocupa 1 Mb, el ejecutable del servidor, compilándolo de esta forma me ocupa sólo 200 Kb y encima me funciona igual de bien o mejor, sin el más mínimo problema.

Referencia de los parametros al compilador::

-s Es una opción del enlazador que quita todas las tablas de símbolos del ejecutable, entre otras cosas.

O3 Optimizar al más alto nivel, se genera código específico de la máquina.

O2Nivel de optimización por defecto.

Lista de parámetros para -march:

i386 Intel 386
i486 Intel/AMD 486
pentium Intel Pentium
pentiumpro Intel Pentium Pro
pentium2 Intel PentiumII/Celeron
pentium3 Intel PentiumIII/Celeron
pentium4 Intel Pentium 4/Celeron
k6 AMD K6
k6-2 AMD K6-2
K6-3 AMD K6-3
athlon AMD Athlon/Duron
athlon-tbird AMD Athlon Thunderbird
athlon-4 AMD Athlon Version 4
athlon-xp AMD Athlon XP
athlon-mp AMD Athlon MP
winchip-c6 Winchip C6
winchip2 Winchip 2
c3 VIA C3 Cyrix

Cada una de ellas son específicas a ese procesador, por ejemplo, si sólo se va a correr software sobre su máquina, es recomendado aplicarlas; sin embargo, si transporta software, digamos desde un P3 a un P4, no se deberían hacer optimizaciones, ya que el binario no funcionaría.

Espero que este artículo les sirva de mucho para optimizar su software a la medida de su PC.

Para saber más...



Artículos relacionados


No hay comentarios: