martes, 19 de agosto de 2014

Debugging - Optimizacion (II)

En el anterior artículo vimos como modificando las opciones de compilador podíamos lograr una notable mejora en el tiempo de ejecución de un programa. (Sobre todo pensemos en programas batch que en cada ejecución deben de procesas cientos de miles de registros: Grandes entidades financieras, Agencias estatales de tributación, seguridad social, etc, etc.).

Vamos a analizar el código ensamblador que nos genera cada una de las compilaciones. Veamos primero el del programa compilado sin opciones de optimizaciòn:



Vemos la carga en la pila de los tres parámetros de la función y a continuación la llamada a la función sumaternaria().

Y en la imagen:



Vemos claramente las operaciones de suma en las líneas 17 a 21 y en la la línea 26 la instrucción return que nos lleva al programa principal.

Hasta aquí no hay nada no esperado.

Ahora vamos al listado ensamblador del programa compilado con la opción de máxima optimización. Y veamos si ha habido una mejora en el código de la función sumaternaria()




Vemos que hay una diferencia la suma ocupa menos instrucciones y opera directamente sobre la pila en vez de andar cargando  los valores en los registros y operando aritméticamente entre los registros. ¿Justificará este ahorro de instrucciones la diferencia de tiempos?.

Vayamos a la línea principal del programa para ver si  también hay diferencias:



La principal diferencia es que no hay llamada a la función sumaternaria(), ¿como es posible?. Antes de ver lo que está ocurriendo la primera conclusión es que si no hay llamada a la función las mejoras que hemos comentado antes tampoco tienen efecto. ¿De donde sale ese ahorro de tiempos?.

Fijémonos en las lineas 53, 54 y 55.. Este código no es más que la carga de parámetros para función printf() y la primera de ellas lo que hace es cargar en la pila el valor 6. Justamente 6 es la suma de los tres números que pasamos como parámetros a la función sumaternaria(). Para estar seguro de ello, volvemos a compilar el programa con la opción -O3 pero esta vez la llamada a la función será con otros valores:

sumaternaria(11, 12, 13);

Y buscamos en el nuevo listado



Vemos como esta vez se carga el valor 36 que corresponde a la suma de 11, 12, y 13. Con lo que confirmamos que el compilador carga directamente el valor de la suma.

Conclusión.


El compilador detecta que se está llamando a una función con valores fijos y coloca directamente el resultado en la pila para la siguiente función.

Por muchas razones: ciclos de cpu, paginación, etc esta codificación si justifica el ahorro de tiempo que hemos visto en las pruebas y, en determinadas condiciones, podría haber sido incluso más.

En el próximo artículo entraremos un poco a analizar los grupos de opciones y sacaremos alguna conclusiones más que deberemos tener en cuenta a la hora de optimizar nuestros programas.




lunes, 4 de agosto de 2014

Debugging - Optimizacion (I)

Empezamos una nueva serie de artículos  de la categoría de Debugging que va a tratar de la optimización de programas.

Todos tenemos claro que el objetivo de optimizar es realizar:  más cantidad / más rápido / mejor calidad con los  mismos o menores recursos.

En la caso de la programación la optimización pasa principalmente por hacer que los programas hagan sus tareas en el menor tiempo y consumiendo los menos recursos, aunque normalmente y a partir de determinado punto de finura ambos conceptos son antagónicos y hay que elegir.

Vamos a centrarnos es esta serie en la optimización como mejora de los tiempos de ejecución. Y vamos a ver como los compiladores nos ayudan en esta tarea.

Aunque siempre lo recuerdo, esta vez hago una vez más hincapié en ello: todos los resultados que se pueden obtener dependen en mayor medida del compilador que estemos usando y, en menor medida, de la versión del mismo.

La parte práctica de este articulo está hecha en el compilador GCC  Version: 4.8.1


En este enlace  tenemos todas las opciones de compilación de GCC, que no son pocas, y aunque se pueden tomar de forma independiente quienes han diseñado el compilador han estimado hacer tres grupos: 1, 2 y 3.
Además tenemos el 0 que quiere decir ninguna optimización y el s que quiere decir optimización de tamaño.

Cada grupo tiene sus propias opciones más las opciones del grupo anterior.

Vamos a trabajar con un sencillo programa que compilaremos, para no andarnos con sutilezas con las opciones más extremas: -O0 y -O3

El programa en cuestión es muy sencillo:

#include <stdio.h>
int sumaternaria( int a, int b, int c )
{
return a + b + c;
}
int main( void )
{
int a = 1;
int b = 2;
int c = 3;
int z = 0;
double  i,j;
for (i=0; i<10000000; i++) 
{   
for (j=0; j<100000; j++)
{
z = sumaternaria( 1, 2, 3);
}
}
printf( "Answer is: %d\n", z );
return 0;
}

Y parece díficil que se pueda optimizar. Este programa realiza una suma de tres número e imprime su resultado. Los dos bucles externos son para crear las suficientes iteraciones que puedan hacer el resultado perceptible.


Para poder trabajar he usado el compilador  y además de usar la opcion -O he utilizad la opción -S para que saque el listado en ensamblador que luego sirve de entrada al ensamblador propiamente dicho.

Además de la opción -O  para optimización, he usado la opción -S (mayúscula, no confundir con -s) para obtener el listado en ensamblado.

Para facilitar el trabajo he creado este script de compilación.

_programa=$1
_opcion=$2
echo "Compilando...."
gcc -O$_opcion -S -g -o $_programa$_opcion.s $_programa.c
echo "Ensamblando...."
as -alh -o $_programa$_opcion.obj $_programa$_opcion.s > $_programa$_opcion.asm
echo "Creando el ejecutable"
gcc $_params  -O$_opcion    -g -o $_programa$_opcion.exe  $_programa.c

Que es invocado de la siguiente forma:

./gcccomp.sh nombre_programa [0|3]

Obtengo los siguientes ficheros:

Programa compilado con la opción -O0
  • nombreprograma0.exe
  • nombreprograma0.asm
Programa compilado con la opción -O0
  • nombreprograma3.exe
  • nombreprograma3.asm

He realizado varias  compilaciones y ejecuciones modificando los valores de la variable j y dejando la variable i sin modificar. En mi equipo obtuve los siguientes resultados:

j = 1000

  • Compilación -O0: 37 segundos
  • Compilación -O3:  < 1 segundo


j: 10000

  • Compilación -O0: 375 segundos
  • Compilación -O3:  129 segundos


j: 100000 

  • Compilación -O0: 93 minutos
  • Compilación -O3:  42 minutos
Ya solo nos queda ver como se ha operado este "milagro" pero eso lo veremos en el siguiente artículo. Mientras tanto animo a los lectores que van siendo cada vez más a realizar las misma pruebas en sus equipos, (se puede realizar tanto en Windows como Linux), y a comparar los resultados obtenidos.