¿Por qué cambiar 0.1 f a 0 ralentiza el rendimiento en 10x?


¿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; // <--
    }
}

Ejecutar 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. (No he probado con otros compiladores.)

Author: Peter Mortensen, 2012-02-16

5 answers

Bienvenido al mundo de punto flotante denormalizado ! ¡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 en coma flotante. Operaciones en punto flotante desnormalizado pueden ser decenas a cientos de veces más lento que en punto flotante normalizado. Esto se debe a que muchos procesadores no pueden manejarlos directamente y deben atrapar y resolver 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 utiliza 0 o 0.1.

Aquí está el código de prueba compilado en x64:

int main() {

    double start = omp_get_wtime();

    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];
#ifdef FLOATING
            y[i]=y[i]+0.1f;
            y[i]=y[i]-0.1f;
#else
            y[i]=y[i]+0;
            y[i]=y[i]-0;
#endif

            if (j > 10000)
                cout << y[i] << "  ";
        }
        if (j > 10000)
            cout << endl;
    }

    double end = omp_get_wtime();
    cout << end - start << endl;

    system("pause");
    return 0;
}

Salida:

#define FLOATING
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007

//#define FLOATING
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.46842e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.45208e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044

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 tratan de manejarlos de manera eficiente.


A demostrar que esto tiene todo que ver con números desnormalizados, si vaciamos los números denormales a cero agregando esto al inicio del código:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

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 sea compilado con SSE habilitado.)

Esto significa que en lugar de usar estos valores raros de precisión casi cero, simplemente redondeamos a cero en su lugar.

Horarios: Core i7 920 @ 3.5 GHz:

//  Don't flush denormals to zero.
0.1f: 0.564067
0   : 26.7669

//  Flush denormals to zero.
0.1f: 0.587117
0   : 0.341406

Al final, esto realmente no tiene nada que ver con si es un entero o punto flotante. El 0 o 0.1f se convierte/almacena en un registro fuera de ambos bucles. Así que eso no tiene ningún efecto en el rendimiento.

 1483
Author: Mysticial,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-17 01:19:26

Usando gcc y aplicando una diferencia al ensamblado generado solo se obtiene esta diferencia:

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0

El cvtsi2ssq es 10 veces más lento de hecho.

Aparentemente, la versión float usa un registro XMM cargado desde la memoria, mientras que la versión int convierte un valor real int 0 a float usando la instrucción cvtsi2ssq, tomando mucho tiempo. Pasar -O3 a gcc no ayuda. (gcc version 4.2.1.)

(Usar double en lugar de float no importa, excepto que cambia el cvtsi2ssq en un cvtsi2sdq.)

Update

Algunas pruebas adicionales muestran que no es necesariamente la instrucción cvtsi2ssq. Una vez eliminado (usando un int ai=0;float a=ai; y usando a en lugar de 0), la diferencia de velocidad permanece. Así que @ Mysticial tiene razón, las carrozas desnormalizadas marcan la diferencia. Esto se puede ver probando valores entre 0 y 0.1f. El punto de inflexión en el código anterior es aproximadamente en 0.00000000000000000000000000000001, cuando los bucles de repente toma 10 veces como largo.

Actualizar

Una pequeña visualización de este interesante fenómeno:

  • Columna 1: un flotador, dividido por 2 para cada iteración
  • Columna 2: la representación binaria de este flotador
  • Columna 3: el tiempo necesario para sumar este flotador 1e7 veces

Puede ver claramente que el exponente (los últimos 9 bits) cambia a su valor más bajo, cuando se establece la desnormalización. En ese punto, la suma simple se convierte en 20 veces más despacio.

0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms
0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms
0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms
0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms
0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms
0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms
0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms
0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms
0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms
0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms
0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms
0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms
0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms
0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms
0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms
0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms
0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms
0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms
0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms
0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms
0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms
0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms
0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms
0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms
0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms
0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms
0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms
0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms
0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms
0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms
0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms
0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms
0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms
0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms
0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms
0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms
0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms

Se puede encontrar una discusión equivalente sobre ARM en la pregunta de desbordamiento de pila¿Punto flotante desnormalizado en Objective-C?.

 399
Author: mvds,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:23

Se debe al uso de punto flotante denormalizado. ¿Cómo deshacerse tanto de ella como de la penalización por rendimiento? Después de haber buscado en Internet formas de matar números denormales, parece que no hay una "mejor" manera de hacer esto todavía. He encontrado estos tres métodos que pueden funcionar mejor en diferentes entornos:

  • Podría no funcionar en algunos entornos GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Podría no funcionar en algunos entornos de Visual Studio: 1

    // Requires #include <xmmintrin.h>
    _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );
    // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both.
    // You might also want to use the underflow mask (1<<11)
    
  • Parece funcionar tanto en GCC como en Visual Studio:

    // Requires #include <xmmintrin.h>
    // Requires #include <pmmintrin.h>
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
    
  • El compilador Intel tiene opciones para deshabilitar los valores denormales de forma predeterminada en las CPU Intel modernas. Más detalles aquí

  • Conmutadores de compilador. -ffast-math, -msse o -mfpmath=sse deshabilitará los denormales y hará algunas otras cosas más rápidas, pero desafortunadamente también hará muchas otras aproximaciones que podrían romper su código. Prueba con cuidado! El equivalente de rápido-matemáticas para el Visual Studio compiler es /fp:fast pero no he podido confirmar si esto también deshabilita los denormales.1

 29
Author: fig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-23 15:53:53

En gcc puede habilitar FTZ y DAZ con esto:

#include <xmmintrin.h>

#define FTZ 1
#define DAZ 1   

void enableFtzDaz()
{
    int mxcsr = _mm_getcsr ();

    if (FTZ) {
            mxcsr |= (1<<15) | (1<<11);
    }

    if (DAZ) {
            mxcsr |= (1<<6);
    }

    _mm_setcsr (mxcsr);
}

También use interruptores gcc:- msse-mfpmath=sse

(créditos correspondientes a Carl Hetherington [1])

[1] http://carlh.net/plugins/denormals.php

 19
Author: German Garcia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-02 04:40:26

El comentario de Dan Neely debería ampliarse en una respuesta:

No es la constante cero 0.0f la que se desnormaliza o causa una ralentización, son los valores que se acercan a cero 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 los valores y[i]. (Se acercan a cero porque x[i]/z[i] es menor que 1.0 para todos i.)

La diferencia crucial entre el lento y las versiones rápidas del código es la instrucción y[i] = y[i] + 0.1f;. Tan pronto como esta línea se ejecuta cada iteración del bucle, la precisión adicional en el flotador se pierde, y la desnormalización necesaria para representar esa precisión ya no es necesaria. Después, las operaciones de coma flotante en y[i] permanecen rápidas porque no están desnormalizadas.

¿Por qué se pierde la precisión extra cuando se agrega 0.1f? Porque los números de coma flotante solo tienen tantos dígitos significativos. Digamos que tienes suficiente espacio de almacenamiento para tres dígitos significativos, luego 0.00001 = 1e-5 y 0.00001 + 0.1 = 0.1, al menos para este ejemplo de formato flotante, porque no tiene espacio para almacenar el bit menos significativo en 0.10001.

En resumen, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; no es el no-op que se podría pensar que es.

Mystical también dijo esto: el contenido de las carrozas importa, no solo el código de la asamblea.

 2
Author: remicles2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-01 13:53:29