«

»

30th marzo 2012

Como optimizar el código usando el compilador GCC

Ahora que últimamente estamos hablando tanto de GCC hemos pensando que seria una buena idea hacer un tutorial sobre como compilar con GCC en sistemas GNU/Linux, Unix o derivados y optimizar el código compilado para nuestra maquina en concreto. Hace tiempo en Leanuxeros escribimos un tutorial sobre como compilar un kernel en Linux, y a pesar de que es perfectamente utilizable a pesar del tiempo que ha pasado, estaría bien escribir otro en el futuro aplicando también optimizaciones para nuestro sistema.

Sin embargo en este tutorial solo comentaremos las opciones de GCC relacionadas con las opciones disponibles en el código fuente de los programas, no haremos mención al kernel Linux esta vez. El tutorial también se combina con documentación de aprendizaje y el articulo puede resultar un poco largo, pero de lectura obligatoria si nos interesa el mundo GNU/Linux y la eficiencia del software.

Antes de empezar seria buena idea hacer un repaso de las funciones principales de GCC para que los usuarios menos experimentados se hagan una mejor idea de las características principales del compilador GCC. Bien, el compilador es muy complejo y esta repleto de funciones que en muchos casos son hasta engorrosas de la cantidad de opciones que ofrecen, a mi personalmente me gusta la eficiencia e ir a lo sencillo. Me gusta mantener una relación de esfuerzo/tiempo/optimización adecuada, siempre he mantenido que optimizar el código merece la pena y mucho, pero siempre hay un limite donde pasar de esa linea os va a costar demasiado tiempo en función de la poca optimización extra que podáis sacar. Como a lo largo de los años he trabajado con maquinas muy potentes y al mismo tiempo con maquinas limitadas en recursos, la optimización era un paso necesario ademas de un ejercicio practico muy saludable para programadores o futuros programadores.

Primero tenemos que tener claro que es lo que significa “optimización”, los milagros no existen y no se pueden meter 5 litros de agua en una botella de 1 litro, sin embargo la “magia” de la optimización consiste en que un programa realice la misma tarea con el menor numero de lineas de código posible, cuantas mas lineas de código tenga el programa, mas tardara la compilación, mas pesara el binario y mas lento realizara las funciones internas. Existen muchas formas de optimizar. Podemos recortar funciones pesadas para maquinas con menos recursos, podemos intentar escribir lo mismo en menos lineas aunque suponga un reto extremadamente difícil, dando a suponer que el programador conozca a la perfección el lenguaje con el que esta trabajando y un largo etc.

A estas alturas estaréis pensando ¿pero esto no era un tutorial para usuarios de sistemas Unix?, si, estáis en lo cierto, no necesitamos ni comprender ni saber programar para optimizar el código fuente desde la perspectiva de un usuario, pero ayuda mucho a entender la optimización si nos ponemos en la piel de un programador.

La segunda fase de optimización que no tiene nada que ver con lo explicado en el párrafo anterior, consiste en convertir el código fuente en código maquina optimizado concretamente para nuestro procesador. Independientemente si el código se ha escrito para beneficiarse de funciones extra para una arquitectura en concreto, podemos reordenar el código para ser mucho mas eficiente en nuestro procesador. Ejemplos básicos los podemos explicar imaginando dos escenarios. Tenemos dos ordenadores ejecutando GNU/Linux, en una maquina tenemos un procesador IBM Power6 que no dispone de ejecución fuera de orden, ademas tampoco contiene un chip de predicción de saltos pero si que tiene una eficiencia extrema de paralelización de tareas y una memoria cache enorme para cumplir esa tarea correctamente. En otra maquina tenemos un procesador Intel Core i7 con ejecución fuera de orden, chip de predicción de saltos y una memoria cache mas limitada pero suficiente para la arquitectura x86_64. Ahora queremos compilar un programa del cual tenemos el código fuente en una carpeta, si realizamos una compilación genérica y creamos los binarios para las arquitecturas ppc64 y x86 se ejecutaran en ambas maquinas pero ninguna de ellas estarán aprovechadas, incluso el programa se arrastraría en el Power6 al estar compilado para ejecutarse fuera de orden, el Power6 acabaría pareciéndose a un Pentium 2 por ejecutar todo cuando no toca y no usando ninguna función que caracteriza a ese procesador.

Sin embargo si le indicamos a GCC que queremos que convierta el código en código predecible y aproveche las optimizaciones disponibles en el procesador Power6, el programa ira a su máximo rendimiento y aprovechara todo lo posible de la arquitectura Power6. Lo mismo en el caso del procesador Intel. Cuanto mas especifica sea la optimización para nuestro sistema, y menos genérica la estemos dejando, estamos limitando la ejecución de ese programa unicamente para nuestra maquina, incluso teniendo dos ordenadores con la misma familia de procesadores pero modelos diferentes, si realizamos una optimización profunda, se convertirían en incompatibles entre si, por esa razón el 99% de los paquetes que encontramos precompilados son genéricos para que funcionen en todas las maquinas soportadas. Es imposible que un sistema venga optimizado al 100% para nuestra maquina, por que incluso con una maquina similar seria casi imposible garantizar el funcionamiento suponiendo que todo el mundo tuviera el mismo procesador, cosa que obviamente no es así.

Las opciones de optimización disponibles en GCC son las siguientes:

-O0 = Es el nivel cero de optimización, no aplica ninguna optimización especial y el código se compilara de forma genérica.

-O1 = Es el nivel mas básico de optimización, el compilador creara código mas rápido, mas ligero sin costar mucho esfuerzo extra al procesador. Ademas de garantizar la compatibilidad en procesadores de la misma familia y generación.

-O2 = Es el primer nivel de optimización profunda, normalmente la mas utilizada. Este nivel optimiza el código utilizando todas las instrucciones disponibles (depende de -march), produciendo código mucho mas rápido y muy ligero pero con un coste de tiempo extra en la compilación.

-O3 = Es el nivel mas profundo de optimización y también el mas arriesgado. Tener como predeterminada esta opción para todo nuestro software es un error ya que hace extremadamente difícil la depuración o directamente imposible en el caso de encontrar un error. El tiempo de compilación puede ser de hasta el doble con esta opción, pero el código puede llegar a pesar la mitad y consumiendo la mitad de memoria. Esta opción es solo recomendable para aplicaciones pesadas que no tengan relación con el kernel. La diferencia respecto a -O2 no es tanta y el riesgo de fallo aumenta considerablemente.

-Os = Este nivel de optimización esta reservada para el tamaño del código, activa todas las opciones -O2 menos las que incrementen el tamaño del código. Es la opción claramente recomendad para maquinas antiguas con procesadores de poca memoria cache o pocos recursos en general, como por ejemplo un Pentium 3 o un Pentium 4. Usar esta opción en equipos modernos también mejora la eficiencia en general, pero sigue siendo mas recomendable usar -O2. En portátiles con Core Duo o Core 2 Duo la mejora a veces es mejor usando -Os

Estas son las opciones de optimización básicas donde no interviene el tipo de arquitectura del procesador, estas opciones hay que saber combinarlas con las opciones de optimización de arquitectura para generar el código adecuado, a continuación vamos a repasar las opciones de arquitectura.

Como hemos dicho en la descripción de -O2, utilizar las instrucciones correctas de nuestro procesador depende de -march, esta opción hace mención a la arquitectura y plataforma de nuestro procesador y también depende de -mtune donde se especifican las instrucciones extra que pueda tener nuestro procesador como el set MMX y SSE en procesadores x86. Como las arquitecturas y opciones soportadas en el compilador GCC son inmensas, solo vamos a comentar las mas comunes de la arquitectura x86 ya que prácticamente cualquier persona que lea este articulo estará utilizando un procesador x86.

-march=native = Esta característica esta disponible desde la versión 4.X de GCC, el compilador comprueba que procesador tenemos y aplica las optimizaciones correspondientes, esto es útil si no sabemos que procesador estamos utilizando en el caso de compilar en una maquina que no sea nuestra. Es la opción recomendada si estamos compilando para nuestra maquina.

-march=i386 = Para procesadores Intel 386, el grado de compatibilidad máxima.

-march=i486 = Para procesadores Intel 486.

-march=i586, pentium = Para procesadores Intel Pentium sin instrucciones MMX.

-march=pentium-mmx = Para procesadores Intel Pentium con instrucciones MMX.

-march=pentiumpro = Para procesadores Intel Pentium Pro.

-march=i686 = Lo mismo que la opción generic pero optimizado para la familia de procesadores i686, ofrece un grado de optimización superior a i386, i486 o i586. Ideal para procesadores como el Pentium M o Core Duo que aun están basados en la arquitectura P6 de Intel.

-march=pentium2 = Para procesadores Intel Pentium 2 con instrucciones MMX.

-march=pentium3, pentium3m = Para procesadores Intel Pentium 3 basados en Pentium Pro con MMX y SSE.

-march=pentium-m = Para procesadores Intel Pentium M basados en i686 (Usados en Centrino), con instrucciones MMX, SSE y SSE2.

-march=pentium4, pentium4m = Para procesadores Intel Pentium 4 con instrucciones MMX, SSE y SSE2.

-march=prescott = Para procesadores Intel Pentium 4 con instrucciones MMX, SSE, SSE2 y SSE3.

-march=nocona = Para procesadores Intel Pentium 4 con extensiones de 64-Bits y con instrucciones MMX, SSE, SSE2 y SSE3.

-march=core2 = Para procesadores Intel Core 2 con extensiones de 64-Bits y con instrucciones MMX, SSE, SSE2, SSE3 y SSSE3.

-march=corei7 = Para procesadores Intel Core i7 con extensiones de 64-Bits y con instrucciones MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1 y SSE4.2.

-march=corei7-avx = Para procesadores Intel Core i7 con extensiones de 64-Bits y con instrucciones MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, AES y PCLMUL.

-march=core-avx-i = Para procesadores Intel Core con extensiones de 64-Bits y con instrucciones MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, AES, PCLMUL, FSGBASE, RDRND y F16C.

-march=atom = Para procesadores Intel Atom con instrucciones MMX, SSE, SSE2, SSE3 y SSSE3.

-pipe = Este prefijo no tiene consecuencias en el código generado por el compilador pero hace la compilación mas rápida por que le indica al procesador utilizar particiones temporales en la memoria RAM donde se reservan bloques pertenecientes a cada hilo de proceso que tengamos disponibles en nuestro o nuestros procesadores. No es recomendable si nuestro equipo tiene poca memoria RAM como por ejemplo 1GB o menos. Para la mayoría de usuarios es una opción recomendable.

En las optimizaciones de las plataformas de arquitectura no hemos indicado las de AMD ya que normalmente son menos comunes y existen muchas, para no hacer el articulo mas extenso de lo necesario, las indicadas para procesadores Intel son las mas utilizadas, si necesitáis saber alguna extensión en particular para vuestro procesador si no esta indicada en la lista, preguntadlo en los comentarios y os escribiremos la extensión que corresponda a vuestra plataforma.

Ahora que ya hemos dejado clara la parte teórica, vamos a poner un ejemplo practico con el código fuente del emulador de Super Nintendo, BSNES. Por si no lo conocéis es uno de los mejores emuladores de Super Nintendo que es multiplataforma y por supuesto, software libre. Nos descargamos el código fuente de la pagina oficial y descomprimimos el archivo donde queramos hasta obtener la carpeta con el código fuente dentro. Abrimos la terminal y nos dirigimos a la carpeta bsnes que se encuentra en la carpeta raíz de la carpeta que acabamos de descomprimir.

Mi procesador es un Intel Core i7 Sandy Bridge de ultima generación, así que en el ejemplo me basare en mi procesador. Para compilar el programa escribimos esto en la terminal:

sudo make CFLAGS=”-march=corei7-avx -O2 -pipe” -j9

Vamos a hacer un repaso de la sintaxis, make da la orden de compilar si existe un archivo preparado Makefile como es en el caso de BSNES, CFLAGS indica que opciones tenga en cuenta el compilador GCC a la hora de compilar. Lo ultimo que esta escrito -j9 no tiene que ver con el compilador, simplemente la j viene de jobs indicando cuantos procesos paralelos quiero que ejecute. Como mi procesador tiene cuatro núcleos físicos y cuatro hilos extra gracias a la tecnología Hyper-Threading en total dispongo de 8 hilos, el noveno esta relacionado con la teoría de colas, como esto daría para otro articulo remitimos un articulo que escribimos hace tiempo en Leanuxeros sobre la teoría de colas en los procesadores.

Una vez que haya terminado de compilar, instalamos con:

sudo make install

Y finalmente tenemos el programa instalado. El articulo se centra en la optimización del código y en este caso hemos puesto como ejercicio practico compilar el emulador BSNES, obviamente esto es aplicable a cualquier programa y ahora BSNES estará lo mas optimizado posible para ejecutarse en mi procesador. Como mi procesador va sobrado para este programa en concreto la necesidad de optimización no es tan importante aunque siempre se agradece. Pero si tenemos una maquina antigua o de bajos recursos la optimización se convierte en algo imprescindible si queremos que nuestro equipo responda con bajas latencias y cifras altas de Gflops.

14 comentarios

3 pings

  1. Superuser escribió:

    Muy buen artículo, la verdad, añadir que para más información en la página oficial de Gentoo está casi todo muy bien explicado buscando por la documentación. Una duda, tengo un AMD Athlon X2, la duda es sobre si podría manejar 3 hilos de proceso. Salu2.

    Positivo o Negativo: Thumb up 1 Thumb down 0

  2. Superuser escribió:

    Vale, tengo una duda urgente— intentando compilar Banshee 2.4 introduzco la línea sudo make CFLAGS=”-march=Athlon64-O2 -pipe” -j2
    El caso es que por algún motivo que no alcanzo a comprender al hacer eso se hace también rm -rf /urs/sbin. Las consecuencias ya os las podeis imaginar, adiós Shell, adiós Synaptic— necesito al menos una hipótesis razonable de lo que ha podido pasar.

    Positivo o Negativo: Thumb up 1 Thumb down 0

  3. Makova escribió:

    Hola Leanux.
    Impresionante, hace falta un foro, espero con ganas ese día :)
    Un millón de gracias por hacer algo tan complicado en un excelente tutorial y de lectura obligatoria para los que estamos comenzando.
    Saludos—

    Positivo o Negativo: Thumb up 1 Thumb down 0

  4. Leanux Xenos escribió:

    @Superuser

    Perdona el retraso. Pues respecto a los hilos siempre es relativo, pero normalmente añadir un hilo extra es completamente seguro, aunque también depende del tipo de código a compilar, hay código que no contiene tanta información como para ser paralelizada y dividirlo en mas hilos afectaría al tiempo de compilación.

    Igualmente el hilo extra también es para gestionar mejor el código, me explico. A veces hay módulos o parte del código pesados que tardan unos segundos de mas en compilarse, eso quiere decir que en el tiempo que se transfiere a memoria RAM y se procesa, hay unos segundos muertos en el que uno de los hilos no trabaja. Si indicamos un numero mayor de trabajos, en el tiempo que esa información va a la memoria cache del procesador aprovecha para compilar otro directorio del mismo código fuente. Por esa razón compilando cosas pesadas como el kernel Linux, segundo a segundo pueden llegar a ser minutos de diferencia sabiendo administrar bien los trabajos.

    Personalmente no he trabajando nunca con AMD y no conozco bien la forma en como trabajan las plataformas de AMD respecto a cache y tipo de instrucciones. Pero en principio es o debería ser muy similar a cualquier plataforma Intel equivalente en generación (menos el ultimo Bulldozer), así que lo recomendable en tu caso seria entre 2 y 4 hilos en función de la complejidad del código.

    Positivo o Negativo: Thumb up 0 Thumb down 0

  5. Leanux Xenos escribió:

    @Makova

    Gracias, es un placer escribir estos artículos sabiendo que otros usuarios están interesados en aprender o curiosear. El foro esta en camino pero estamos escribiendo gran parte del código nosotros para integrarlo a la web. Todavía esta muy verde la cosa, esperamos poder hacer el cambio en la web antes de Junio :D

    Positivo o Negativo: Thumb up 1 Thumb down 0

  6. Leanux Xenos escribió:

    @Superuser

    Todavia estoy intentando ver cual es el problema. A pesar de utilizar configuraciones muy diferentes, voy a intentar replicar tu problema y buscar una respuesta logica al problema.

    Positivo o Negativo: Thumb up 0 Thumb down 0

  7. Superuser escribió:

    Muchas gracias por la aclaración de los hilos de proceso, respecto al problema— me parece que no volveré a compilar sin probar primero en una máquina virtual, ahora no puedo ni logearme en mi Linux, en fin, no se si se debió a los parámetros que le pasé a GCC, cosa difícil, o a algo que pudo ejecutarse en la carpeta del código— :S en fin, aprovecharé para probar alguna distro, ya que tengo que reinstalar.

    Positivo o Negativo: Thumb up 1 Thumb down 0

  8. Leanux Xenos escribió:

    Ciertamente es un caro bastante raro, escribiendo lo que has escrito como salida en el compilador, dudo que este tenga nada que ver— me habre cansado de compilar yo en todos estos años y no he visto nada similar. Aunque si que puede ser una opcion mal ejecutada por parte de Banshee, quizas estaba programado para eliminar algun rastro en el caso de existir alguna version antigua, o algun tema similar.

    A ver si despues puedo ponerme y descubrir algo aunque sea para tenerlo en cuenta en el futuro.

    P.D: Por curiosidad ¿que distro usabas o te apetece probar?

    Positivo o Negativo: Thumb up 0 Thumb down 0

  9. Superuser escribió:

    Usaba Debian, y me estoy bajando Mint más que nada por que necesito algo Out of the Box, el Lunes tengo examen y el tiempo apremia, pero no es lo que más me gustaría, en fin, la verdad es que no se cual probar, al final me da igual si es KDE, ¿Cuál usais o recomendais por aquí?

    Positivo o Negativo: Thumb up 1 Thumb down 0

  10. Leanux Xenos escribió:

    Personalmente no me gusta Mint, para usar alguna distro basada en Debian utilizaria directamente Ubuntu que es lo mas parecido a la filosofia “Out of The Box”, y ademas le puedes instalar KDE, Xfce etc etc de forma facil en los repositorios si no te gusta Unity.

    Como personalmente estuve utilizando Red Hat durante muchos años, a pesar de que soy un fan acerrimo de Debian, todavia siento algo de debilidad por Red Hat que ahora no existe como tal y se convritio en Fedora.
    Fedora es otra solucion “Out of The Box” muy buena potente, y con un soporte muy maduro, de hecho casi que preferiria usar Fedora a Ubuntu por las herramientas que trae en general, y el sistema de paquetes Red Hat .RPM tambien es una delicia.

    Positivo o Negativo: Thumb up 1 Thumb down 0

  11. Superuser escribió:

    Si, Fedora siempre me ha gustado mucho, pero bueno, es atreverme con Gnome 3, otras veces que he instalado Mint ha sido en algunas de sus variantes como LMDE y funcionaba fatal. Por otro lado hoy en día lo más parecido a Red Hat puede que sea CentOS, pero está demasiado desactualizado para mi gusto, creo que le daré otra oportunidad a Fedora, aunque seguramente lo instale con KDE.

    Positivo o Negativo: Thumb up 1 Thumb down 0

  12. Leanux Xenos escribió:

    Fedora con KDE para mi es una solucion muy buena, Gnome no me gusta y mucho menos Gnome3. Kubuntu tampoco es una muy buena idea, tiene bastantes fallos tontos y no tiene una buena integracion con KDE (aunque Debian menos xD). SuSE Linux tambien es una muy buena distro, y su gestor YAST es una maravilla, pero sigo prefiriendo Fedora con KDE.

    Positivo o Negativo: Thumb up 0 Thumb down 0

  13. Superuser escribió:

    Bueno, ya está Fedora funcionando con KDE y todo de maravilla, funciona perfectamente, gracias por echar una mano :)

    Positivo o Negativo: Thumb up 2 Thumb down 0

  14. Leanux Xenos escribió:

    ; ), lo tendre en cuenta cuando escriba proximos tutoriales!

    Positivo o Negativo: Thumb up 0 Thumb down 0

  1. Leanuxeros » HardInfo, centro de información para GNU/Linux escribió:

    [...] Recordad que si tenéis un procesador multinúcleo podéis acelerar la compilación paralelizando el trabajo en varios núcleos con “-jobs”, si estáis interesados en aprender mas sobre la paralelización a la hora de compilar quizás os interese algunos de nuestros artículos sobre la compilación en procesadores multinúcleo o aplicar optimizaciones al compilador GCC. [...]

    Positivo o Negativo: Thumb up 0 Thumb down 0

  2. Leanuxeros » “-Og”, nuevo nivel de optimización para el compilador GCC escribió:

    [...] optimizar el código de ejecución lo máximo posible. Hace unos meses en Leanuxeros publicamos un articulo sobre como optimizar el código compilado en GCC, pues teniendo en cuenta aquel articulo podemos añadir unas cuantas [...]

    Positivo o Negativo: Thumb up 0 Thumb down 0

  3. Leanuxeros » Genode OS 12.11, una propuesta a largo plazo escribió:

    [...] plataformas. Después cada sistema operativo debería contar con sus propias herramientas para optimizar el código a la hora de [...]

    Positivo o Negativo: Thumb up 0 Thumb down 0

Deja un comentario