const int UP = 8;
inline int Sign(int color)
{ return (1 - (color * 2)); } // White sign is +1, Black sign is -1
inline int Ahead(int color)
{ return (Sign(color) * UP); } // UP is -8, DOWN is +8
Supongamos que la función Ahead se llama muchas veces siendo color un valor que sólo admite los valores 0 y 1. ¿Es más rápido precalcular un array ahead de dos valores?
En este caso creo que la respuesta es clara. Los cálculos son extremadamente sencillos y pueden hacerse sobre registros, así que ni hablar de precálculos. Aunque para dos únicos valores es seguro que la cache se utilizaría eficientemente, acceder a registros es más rápido que acceder a cache.
Sin embargo aún quedan algunas preguntas por responder:
- ¿Los inline hacen algo?
- ¿Se conseguiría alguna ganancia optimizando a mano Ahead y sign?
La solución está aquí |
Para ver el código que genera el compilador escribí el siguiente programa:
int main()
{
int b=rand() & 1; // Me aseguro de que b=0,1
int a=Ahead(b) ;
printf("Hello world %i !\n", a);
return 0;
}
El rand() es necesario porque si ponemos una constante literal el compilador no se molesta en incluir las funciones y llama a printf con el valor calculado en tiempo de compilación. El printf es necesario porque si no usamos el valor de a, el compilador no se molesta en incluir nada.
El código máquina generado por gcc con la máxima optimización (-O3), pero sin más florituras es:
sub rsp,0x8
call 0x400470 <rand@plt> // Llama a rand()
and eax,0x1 // b = rand() & 1
mov esi,0x4006bc // N.P.I.
mov edi,0x1 // N.P.I.
neg eax // eax = -b
add eax,eax // eax = eax+eax = -2*b
lea edx,[rax*8+0x8] // edx = 8*eax+8 = 8 - 16*b
xor eax,eax // eax = 0
call 0x400460 <__printf_chk@plt> // Llama a printf
xor eax,eax
add rsp,0x8
ret
La parte del programa donde se hace la "llamada" a Ahead son simplemente tres instrucciones en código máquina que realizan la operación a = 8 - 16*b.
Se pueden hacer muchos comentarios respecto al código y la cantidad de trucos que usa:
1. Se da cuenta de que la llamada combinada a las dos funciones se puede simplificar como:
Ahead(color) = (1 - (color * 2)) * UP = 8 - 16*color
2. Aprovecha que teníamos color en eax y lo suma consigo mismo para obtener el doble de una forma rápida y sin necesidad de parámetros adicionales.
3. Usa la instrucción lea (load effective address) como un truco para obtener un cálculo aritmético. Esta función suele usarse para calcular punteros y moverse por un array (índice + offset), pero aquí se usa para hacer un cálculo. Desconozco si es posible escoger cualquier multiplicador o si hay sólo unos cuantos a elegir (lo que explicaría por qué no hace simplemente [rax*16+0x8]).
En resumen, reto a cualquiera a que escriba un código máquina más eficiente que el que genera el compilador para este caso.
Un detalle interesante es que, en este caso, si hubiéramos usado un array, los "cálculos" necesarios para acceder a los elementos son casi igual de complicados que el cálculo en sí.
Respecto a la otra pregunta: pues no, el gcc genera el mismo código. Da igual si hemos puesto la directiva inline o no.
(Continuará)
Pensado en la utilidad de:
ResponderEliminarmov esi,0x4006bc // N.P.I.
mov edi,0x1 // N.P.I.
pienso que deben ser la dirección del string del printf y el número de parámetros (podría comprobarlo, pero no tengo el código a mano). De todas formas, me surge la siguiente pregunta de la que quizás alguien sepa la respuesta.
Parece claro por el código que en la librería existen versiones de printf con parámetros en los registros (junto, supongo, a las normales con push y pop). Sé cómo va eso cuando la función en cuestión está en el propio código que se compila, pero no acabo de ver cómo lo hace el compilador cuando se trata de una función en una librería. ¿Alguna idea?