Humano vs gcc



Hace unos días paso por la portada de :Barrapunto: , un sistema operativo cuyo kernel estaba escrito en ensamblador x86 al 100%.

En uno de los hilos alguien mencionó que no era posible (o que era muy difícil) codificar mejor que el compilador.

Lo cierto es que no suelo escribir por Barrapunto (ni siquiera estoy registrado), pero este comentario hirío mi sensibilidad y no podía dejarlo correr (Sobre todo cuando la gente empezó a darle la razón :-)), así que escribí un pequeño test para demostrar lo contrario, que aún es bastante sencillo "ganar a la máquina".

La idea de fondo es que el compilador no emplea una estrategia de optimización acorde con el programa, simplemente intenta optimizar de la mejor forma posible pequeños retazos de código. No es capaz de ver el conjunto.

Podríamos decir que el compilador se asemeja a un jugador de ajedrez que intenta realizar el mejor movimiento posible evaluando sólo la posición actual. No sigue un plan. No puede ver que lo que parece una jugada "optima" en este momento le conduce a una encerrona dos o tres movimientos después (Tentado por un "máximo relativo"). Y, desde luego, un jugador con este estilo podría ser derrotado por cualquier aficionado con algunas nociones de estrategia.

Un humano, cuando codifica en ensamblador sabe de donde parte, los datos, y a donde debe llegar, el resultado a obtener a través del algoritmo. Puede entonces echar mano de su habilidad para determinar un buen camino entre los dos puntos. Los compiladores no disponen de toda esa información, no entienden de algoritmia ni adivinan lo que pretendemos hacer. Son "ciegos" y por eso un programador humano puede vencerles (aún).

Esta es un copia del correo que ofrecí en Barrapunto.
>

Como ya te habrás imaginado, soy la persona que envió el comentario apuntando que era factible codificar en lenguaje ensamblador mejor que el compilador.

El adjunto de este mensaje es un ejemplito que ilustra lo que quise decir. En bench.c<em> se incluyen tres versiones en C del mismo procedimiento, la suma acumulativa de los elementos de un array de floats.

La primera versión (y la más lenta), utiliza un entero como índice para iterar sobre el array, la segunda emplea punteros (lo que debería acelerar en cualquier compilador de C los desplazamientos sobre arrays hasta un 10%) y la tercera en la que he desenrollado el bucle a mano.

Junto con esas tres funciones, se incluye en opt.asm la versión en ensamblador.

Ésta última, aunque utiliza pocos trucos de optimización: no usa más juego de instrucciones que las del copro estándar (ni 3dnow, ni otras simd sobre reales), sólo he mirado el tema del paralelaje de los pipelines en el bucle interno y en el salto hacia delante, etc... Es sensiblemente más rápida que cualquiera de las propuestas por el compilador.

Antes de nada, mi máquina es un P100 sin mmx, mi compilador gcc-2.95.1 y mi nasm-0.98 (Quizás ahora te expliques el porqué del salto hacia delante :-)).

En segundos, sin usar optimización, el profiler muestra esto:
 seg
 =====
 11.80 func1
 8.76  func2
 7.62  func3
 3.58  _func_asm_interno
 0.56  _func_asm_bucle


Luego func_asm (_func_asm_interno + _func_asm_bucle) vence claramente a nuestra mejor candidata en C (func3).

Activamos ahora algunas mejoras en GCC:
 CFLAGS="-O3 -funroll-all-loops -pg"


Y los nuevos tiempos son:
 seg
 =====
 6.48  func2
 6.43  func1
 6.41  func3
 3.50  _func_asm_interno
 0.71  _func_asm_bucle


Desde luego las optimizaciones han hecho su trabajo: ahora, las tres funciones en C rinden prácticamente igual. La función en ensamblador no se ha movido (Para ella las optimizaciones no tienen efecto), pero aún así se muestra inalcanzable.

Con "MS VisualC++" versión 5.0 los resultados son similares. Te invito a que hagas tus propias pruebas en tu máquina.

Las optimizaciones que efectúa el compilador son "generalistas", no orientadas al problema... Y fijaté que el procedimiento que elegí para la comparativa es de lo más sencillo para el compilador: su traducción a asm debería ser casi directa. Puedes ver el código que propone GCC con la opción "-S".

El compilador "gana" al humano porque no es viable para este último codificar enormes listados en ensamblador sin descuidar la optimización, no porque lo haga mejor en realidad. Gana por "desgaste".

Nota:

Para "visualizar" el ejemplo (que puedes descargar: test-asm.tar.gz ) necesita un compilador de C, el nasm y el gprof.

Incluyo un Makefile para simplificar las cosas:
 $ tar xzf test-asm.tar.gz && cd test-asm
 $ make prof
 $ make rclean
 $ make CFLAGS="-O3 -funroll-all-loops -pg" prof
 $ # etc, etc...