¿Por qué este bit de código,
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0.1f; // <--
y[i] = y[i] - 0.1f; // <--
}
}
ejecuta más de 10 veces más rápido que el siguiente bit (idéntico excepto donde se indique)
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0; // <--
y[i] = y[i] - 0; // <--
}
}
al compilar con Visual Studio 2010 SP1. El nivel de optimización fue -02
con sse2
activado. No he probado con otros compiladores.
0
,0f
,0d
, o incluso(int)0
en un contexto donde unadouble
es necesaria.Respuestas:
¡Bienvenido al mundo del punto flotante desnormalizado ! ¡Pueden causar estragos en el rendimiento!
Los números denormales (o subnormales) son una especie de truco para obtener algunos valores adicionales muy cercanos a cero de la representación de coma flotante. Las operaciones en punto flotante desnormalizado pueden ser decenas a cientos de veces más lentas que en punto flotante normalizado. Esto se debe a que muchos procesadores no pueden manejarlos directamente y deben atraparlos y resolverlos usando microcódigo.
Si imprime los números después de 10,000 iteraciones, verá que han convergido a diferentes valores dependiendo de si se usa
0
o no0.1
.Aquí está el código de prueba compilado en x64:
Salida:
Observe cómo en la segunda ejecución los números están muy cerca de cero.
Los números desnormalizados son generalmente raros y, por lo tanto, la mayoría de los procesadores no intentan manejarlos de manera eficiente.
Para demostrar que esto tiene todo que ver con los números desnormalizados, si volcamos los denormals a cero agregando esto al comienzo del código:
Entonces la versión con
0
ya no es 10 veces más lenta y en realidad se vuelve más rápida. (Esto requiere que el código se compile con SSE habilitado).Esto significa que, en lugar de utilizar estos extraños valores de precisión casi inferior a cero, simplemente redondeamos a cero.
Tiempos: Core i7 920 @ 3.5 GHz:
Al final, esto realmente no tiene nada que ver con si es un entero o un punto flotante. El
0
o0.1f
se convierte / almacena en un registro fuera de ambos bucles. Entonces eso no tiene ningún efecto en el rendimiento.fuente
+ 0.0f
se optimiza. Si tuviera que adivinar, podría ser que+ 0.0f
tendría efectos secundarios si sey[i]
tratara de una señalizaciónNaN
o algo ... Sin embargo, podría estar equivocado.El uso
gcc
y la aplicación de un diff al ensamblaje generado arroja solo esta diferencia:El
cvtsi2ssq
que es 10 veces más lento de hecho.Aparentemente, la
float
versión usa un registro XMM cargado desde la memoria, mientras que laint
versión convierte unint
valor real 0 parafloat
usar lacvtsi2ssq
instrucción, lo que lleva mucho tiempo. Pasar-O3
a gcc no ayuda. (gcc versión 4.2.1.)(Usar en
double
lugar defloat
no importa, excepto que cambia elcvtsi2ssq
acvtsi2sdq
.)Actualizar
Algunas pruebas adicionales muestran que no es necesariamente la
cvtsi2ssq
instrucción. Una vez eliminado (usando ayint ai=0;float a=ai;
usando ena
lugar de0
), la diferencia de velocidad permanece. Entonces @Mysticial tiene razón, los flotadores desnormalizados marcan la diferencia. Esto se puede ver probando valores entre0
y0.1f
. El punto de inflexión en el código anterior es aproximadamente en0.00000000000000000000000000000001
, cuando los bucles de repente tardan 10 veces más.Actualización << 1
Una pequeña visualización de este interesante fenómeno:
Puede ver claramente el exponente (los últimos 9 bits) cambiar a su valor más bajo, cuando se establece la desnormalización. En ese punto, la suma simple se vuelve 20 veces más lenta.
Se puede encontrar una discusión equivalente sobre ARM en la pregunta de desbordamiento de pila ¿ Punto flotante desnormalizado en Objective-C? .
fuente
-O
s no lo arregla, pero lo-ffast-math
hace. (Yo uso todo el tiempo, la OMI los casos de esquina donde causa problemas de precisión no debe aparecer en un programa bien diseñado de todos modos.)-ffast-math
enlaces con un código de inicio adicional que establece FTZ (vaciado a cero) y DAZ (denormal son cero) en el MXCSR, por lo que la CPU nunca tiene que tomar una ayuda lenta de microcódigo para denormals.Se debe al uso de punto flotante desnormalizado. ¿Cómo deshacerse de él y de la penalización de rendimiento? Después de haber buscado en Internet formas de matar los números normales, parece que todavía no hay una "mejor" forma de hacerlo. He encontrado estos tres métodos que pueden funcionar mejor en diferentes entornos:
Podría no funcionar en algunos entornos de CCG:
Podría no funcionar en algunos entornos de Visual Studio: 1
Parece funcionar tanto en GCC como en Visual Studio:
El compilador Intel tiene opciones para deshabilitar denormals por defecto en las CPU Intel modernas. Más detalles aquí
El compilador cambia.
-ffast-math
,-msse
o-mfpmath=sse
deshabilitará los valores normales y acelerará algunas otras cosas, pero desafortunadamente también hará muchas otras aproximaciones que podrían romper su código. Prueba con cuidado! El equivalente de las matemáticas rápidas para el compilador de Visual Studio es,/fp:fast
pero no he podido confirmar si esto también deshabilita los valores normales. 1fuente
En gcc puede habilitar FTZ y DAZ con esto:
también use modificadores gcc: -msse -mfpmath = sse
(créditos correspondientes a Carl Hetherington [1])
[1] http://carlh.net/plugins/denormals.php
fuente
fesetround()
a partir defenv.h
(definido para C99) para otro, de forma más portátil del redondeo ( linux.die.net/man/3/fesetround ) (pero esto afectaría a todas las operaciones de PF, no sólo a los subnormales )El comentario de Dan Neely debería ampliarse a una respuesta:
No es la constante cero
0.0f
que se desnormaliza o causa una desaceleración, son los valores que se acercan a cero en cada iteración del bucle. A medida que se acercan más y más a cero, necesitan más precisión para representar y se desnormalizan. Estos son losy[i]
valores. (Se acercan a cero porquex[i]/z[i]
es menor que 1.0 para todosi
).La diferencia crucial entre las versiones lenta y rápida del código es la declaración
y[i] = y[i] + 0.1f;
. Tan pronto como se ejecuta esta línea en cada iteración del bucle, se pierde la precisión adicional en el flotador y la desnormalización necesaria para representar esa precisión ya no es necesaria. Posteriormente, las operaciones de coma flotantey[i]
permanecen rápidas porque no están desnormalizadas.¿Por qué se pierde la precisión adicional cuando agrega
0.1f
? Porque los números de coma flotante solo tienen tantos dígitos significativos. Digamos que tiene suficiente almacenamiento para tres dígitos significativos, entonces0.00001 = 1e-5
, y0.00001 + 0.1 = 0.1
, al menos para este formato flotante de ejemplo, porque no tiene espacio para almacenar el bit menos significativo0.10001
.En resumen,
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
no es el no-op que crees que es.Mystical también dijo esto : el contenido de los flotadores es importante, no solo el código de ensamblaje.
fuente