c++ - Perché la modifica da 0.1f a 0 rallenta le prestazioni di 10 volte?




performance visual-studio-2010 (4)

Perché questo bit di codice,

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

eseguito più di 10 volte più veloce del seguente bit (identico tranne dove indicato)?

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

durante la compilazione con Visual Studio 2010 SP1. (Non ho testato con altri compilatori.)


È dovuto all'utilizzo in virgola mobile denormalizzato. Come sbarazzarsi di entrambi e della penalità delle prestazioni? Dopo aver perlustrato Internet per modi di uccidere numeri denormali, sembra che non ci sia ancora un modo "migliore" per farlo. Ho trovato questi tre metodi che possono funzionare meglio in ambienti diversi:

  • Potrebbe non funzionare in alcuni ambienti GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Potrebbe non funzionare in alcuni ambienti di 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)
    
  • Sembra funzionare sia in GCC che in 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);
    
  • Il compilatore Intel ha opzioni per disabilitare i denormali di default sulle moderne CPU Intel. Maggiori dettagli qui

  • Interruttori del compilatore. -ffast-math , -msse o -mfpmath=sse disabiliterà i denormals e renderà qualche altra cosa più veloce, ma sfortunatamente fa anche molte altre approssimazioni che potrebbero infrangere il tuo codice. Prova con attenzione! L'equivalente di fast-math per il compilatore di Visual Studio è /fp:fast ma non sono stato in grado di confermare se questo disabilita anche i denormali. 1


In gcc puoi abilitare FTZ e DAZ con questo:

#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);
}

utilizzare anche le opzioni di gcc: -msse -mfpmath = sse

(crediti corrispondenti a Carl Hetherington [1])

[1] 1


Il commento di Dan Neely dovrebbe essere ampliato in una risposta:

Non è la costante zero 0.0f che viene denormalizzata o causa un rallentamento, sono i valori che si avvicinano a zero ogni iterazione del ciclo. Man mano che si avvicinano sempre più allo zero, hanno bisogno di maggiore precisione per rappresentare e diventano denormalizzati. Questi sono y[i] valori di y[i] . (Si avvicinano allo zero perché x[i]/z[i] è inferiore a 1.0 per tutti i .)

La differenza cruciale tra le versioni lente e veloci del codice è l'affermazione y[i] = y[i] + 0.1f; . Non appena questa linea viene eseguita ogni iterazione del ciclo, la precisione extra nel float viene persa e la denormalizzazione necessaria per rappresentare che la precisione non è più necessaria. Successivamente, le operazioni in virgola mobile su y[i] rimangono veloci perché non vengono denormalizzate.

Perché la precisione extra viene persa quando si aggiunge 0.1f ? Perché i numeri in virgola mobile hanno solo tante cifre significative. Supponiamo di avere memoria sufficiente per tre cifre significative, quindi 0.00001 = 1e-5 e 0.00001 + 0.1 = 0.1 , almeno per questo esempio di formato float, perché non ha spazio per memorizzare il bit meno significativo in 0.10001 .

In breve, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; non è il non-op si potrebbe pensare che sia.

Anche Mystical ha detto questo : il contenuto dei float è importante, non solo il codice assembly.


Benvenuti nel mondo del punto di virgola mobile denormalizzato ! Possono devastare le prestazioni !!!

I numeri Denormal (o subnormali) sono una specie di trucco per ottenere valori extra molto vicini allo zero rispetto alla rappresentazione in virgola mobile. Le operazioni su un punto fluttuante denormalizzato possono essere da decine a centinaia di volte più lente rispetto a un punto fluttuante normalizzato. Questo perché molti processori non possono gestirli direttamente e devono intercettarli e risolverli usando il microcodice.

Se si stampano i numeri dopo 10.000 iterazioni, si vedrà che sono stati convertiti in valori diversi a seconda che venga utilizzato 0 o 0.1 .

Ecco il codice di prova compilato su 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;
}

Produzione:

#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

Nota come nella seconda analisi i numeri sono molto vicini allo zero.

I numeri denormalizzati sono generalmente rari e quindi la maggior parte dei processori non tenta di gestirli in modo efficiente.

Per dimostrare che tutto ciò ha a che fare con numeri denormalizzati, se svuotiamo i denormali a zero aggiungendo questo all'inizio del codice:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Quindi la versione con 0 non è più 10 volte più lenta e in realtà diventa più veloce. (Ciò richiede che il codice venga compilato con SSE abilitato).

Ciò significa che piuttosto che utilizzare questi valori quasi zero di precisione inferiore bizzarri, abbiamo invece solo intorno a zero.

Tempi: 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

Alla fine, questo non ha davvero nulla a che fare con un intero o un punto a virgola mobile. Lo 0 o lo 0.1f viene convertito / memorizzato in un registro al di fuori di entrambi i loop. In modo che non ha alcun effetto sulle prestazioni.





floating-point