Perché la modifica da 0,1f a 0 rallenta le prestazioni di 10x?


1528

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

correre 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. Il livello di ottimizzazione era -02con sse2abilitato. Non ho testato con altri compilatori.


10
Come hai misurato la differenza? E quali opzioni hai usato durante la compilazione?
James Kanze,

158
Perché il compilatore non sta semplicemente facendo cadere +/- 0 in questo caso?!?
Michael Dorgan,

127
@ Zyx2000 Il compilatore non è affatto così stupido. Smontaggio un esempio banale in spettacoli LINQPad che sputa fuori lo stesso codice che si utilizzi 0, 0f, 0d, o anche (int)0in un contesto in cui una doubleè necessario.
millimoose,

14
qual è il livello di ottimizzazione?
Otto Allmendinger,

Risposte:


1617

Benvenuti nel mondo del virgola mobile denormalizzato ! Possono rovinare le prestazioni !!!

I numeri denormali (o subnormali) sono una specie di hack per ottenere alcuni valori extra molto vicini allo zero dalla rappresentazione in virgola mobile. Le operazioni su virgola mobile denormalizzate possono essere decine o centinaia di volte più lente rispetto a virgola mobile normalizzata. Questo perché molti processori non possono gestirli direttamente e devono intercettarli e risolverli utilizzando il microcodice.

Se si stampano i numeri dopo 10.000 iterazioni, si noterà che sono stati convertiti in valori diversi a seconda che vengano utilizzati 0o meno 0.1.

Ecco il codice di test 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 serie i numeri sono molto vicini allo zero.

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


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

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

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

Ciò significa che invece di utilizzare questi strani valori di precisione quasi zero quasi zero, invece arrotondiamo a zero.

Tempi: Core i7 920 a 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 il fatto che sia un numero intero o un virgola mobile. L' 0o 0.1fviene convertito / memorizzato in un registro esterno a entrambi i loop. Quindi ciò non ha alcun effetto sulle prestazioni.


100
Sto ancora trovando un po 'strano che "+ 0" non sia completamente ottimizzato dal compilatore per impostazione predefinita. Questo sarebbe successo se avesse messo "+ 0.0f"?
s73v3r,

51
@ s73v3r Questa è un'ottima domanda. Ora che guardo l'assemblaggio, non + 0.0fviene nemmeno ottimizzato. Se dovessi indovinare, potrebbe essere che + 0.0favrebbe effetti collaterali se y[i]fosse una segnalazione NaNo qualcosa del genere ... Potrei sbagliarmi però.
Mistico il

14
I doppi si imbatteranno ancora nello stesso problema in molti casi, solo con una diversa grandezza numerica. Il flush-to-zero va bene per le applicazioni audio (e altre dove puoi permetterti di perdere 1e-38 qua e là), ma credo che non si applichi a x87. Senza FTZ, la solita correzione per le applicazioni audio è quella di iniettare un segnale DC o o di onda quadra di ampiezza molto bassa (o non udibile) a numeri di jitter lontano dalla denormalità.
Russell Borogove,

16
@Isaac perché quando y [i] è significativamente inferiore a 0,1 aggiungendolo si ottiene una perdita di precisione perché la cifra più significativa nel numero diventa più alta.
Dan Fiddling By Firelight,

167
@ s73v3r: + 0.f non può essere ottimizzato perché il virgola mobile ha uno 0 negativo e il risultato dell'aggiunta di + 0.f a -.0f è + 0.f. Quindi l'aggiunta di 0.f non è un'operazione di identità e non può essere ottimizzata.
Eric Postpischil,

415

L'uso gcce l'applicazione di un diff all'assembly generato produce solo questa differenza:

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

L' cvtsi2ssquno infatti è 10 volte più lento.

Apparentemente, la floatversione utilizza un registro XMM caricato dalla memoria, mentre la intversione converte un intvalore reale 0 floatnell'uso cvtsi2ssqdell'istruzione, impiegando molto tempo. Passare -O3a gcc non aiuta. (versione gcc 4.2.1.)

(L'uso al doubleposto di floatnon ha importanza, tranne per il fatto che cambia cvtsi2ssqin a cvtsi2sdq.)

Aggiornare

Alcuni test extra mostrano che non è necessariamente l' cvtsi2ssqistruzione. Una volta eliminato (usando a int ai=0;float a=ai;e usando ainvece di 0), la differenza di velocità rimane. Quindi @Mysticial ha ragione, i float denormalizzati fanno la differenza. Questo può essere visto testando i valori tra 0e 0.1f. Il punto di svolta nel codice sopra è approssimativamente a 0.00000000000000000000000000000001, quando i loop impiegano improvvisamente 10 volte di più.

Aggiornamento << 1

Una piccola visualizzazione di questo interessante fenomeno:

  • Colonna 1: un float, diviso per 2 per ogni iterazione
  • Colonna 2: la rappresentazione binaria di questo float
  • Colonna 3: il tempo impiegato per sommare questo float 1e7 volte

Puoi vedere chiaramente l'esponente (gli ultimi 9 bit) cambiare al suo valore più basso, quando inizia la denormalizzazione. A quel punto, la semplice aggiunta diventa 20 volte più lenta.

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

Una discussione equivalente su ARM è disponibile nella domanda Stack Overflow Denormalized floating point in Objective-C? .


27
-Os non lo risolve, ma lo -ffast-mathfa. (Lo uso sempre, IMO nei casi angolari in cui causa problemi di precisione non dovrebbe comunque presentarsi in un programma adeguatamente progettato.)
leftaroundabout

Non c'è conversione ad alcun livello di ottimizzazione positiva con gcc-4.6.
Jed

@leftaroundabout: compilazione di un eseguibile (non libreria) con -ffast-mathcollegamenti ad un codice di avvio extra che imposta FTZ (flush a zero) e DAZ (denormal are zero) in MXCSR, quindi la CPU non deve mai prendere un microcodice lento per i denormals.
Peter Cordes,

34

È dovuto all'uso denormalizzato in virgola mobile. Come sbarazzarsi sia di esso che della penalità di prestazione? Avendo cercato su Internet modi per uccidere numeri denormali, sembra che non ci sia ancora un modo "migliore" per farlo. Ho trovato questi tre metodi che potrebbero funzionare meglio in diversi ambienti:

  • 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 in GCC e 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 denormals di default sulle moderne CPU Intel. Maggiori dettagli qui

  • Switch del compilatore. -ffast-math, -msseo -mfpmath=ssedisabiliterà i denormals e renderà alcune altre cose più veloci, ma sfortunatamente farà anche molte altre approssimazioni che potrebbero violare il codice. Prova attentamente! L'equivalente di fast-math per il compilatore di Visual Studio è /fp:fastma non sono stato in grado di confermare se questo disabilita anche i denormals. 1


1
Sembra una risposta decente a una domanda diversa ma correlata (Come posso evitare che i calcoli numerici producano risultati denormali?) Tuttavia, non risponde a questa domanda.
Ben Voigt,

Windows X64 passa un'impostazione di underflow improvviso quando avvia .exe, mentre Windows 32-bit e Linux no. Su Linux, gcc -ffast-math dovrebbe impostare un underflow improvviso (ma penso non su Windows). I compilatori Intel dovrebbero essere inizializzati in main () in modo che queste differenze del sistema operativo non passino, ma io sono stato morso e devo impostarlo esplicitamente nel programma. Si suppone che le CPU Intel che iniziano con Sandy Bridge gestiscano in modo efficiente le sub-anomalie derivanti dall'aggiunta / sottrazione (ma non dalla divisione / moltiplicazione), quindi è opportuno utilizzare un underflow graduale.
tim18

1
Microsoft / fp: fast (non un default) non fa nulla di aggressivo inerente a gcc -ffast-math o ICL (default) / fp: fast. È più simile a ICL / fp: source. Quindi è necessario impostare / fp: (e, in alcuni casi, modalità underflow) esplicitamente se si desidera confrontare questi compilatori.
tim18,

18

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

usa anche gli switch gcc: -msse -mfpmath = sse

(crediti corrispondenti a Carl Hetherington [1])

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


Vedi anche fesetround()da fenv.h(definito per C99) per un altro modo più arrotondato di arrotondamento ( linux.die.net/man/3/fesetround ) (ma questo influenzerebbe tutte le operazioni FP, non solo i subnormali )
Garcia tedesca,

Sei sicuro di aver bisogno di 1 << 15 e 1 << 11 per FTZ? Ho visto solo 1 << 15 citato altrove ...
fico

@fig: 1 << 11 è per la maschera di underflow. Maggiori informazioni qui: softpixel.com/~cwright/programming/simd/sse.php
Garcia tedesca,

@GermanGarcia questo non risponde alla domanda dei PO; la domanda era "Perché questo bit di codice, viene eseguito 10 volte più veloce di ..." - dovresti provare a rispondere prima di fornire questa soluzione alternativa o fornirlo in un commento.

9

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

Non è la costante zero 0.0fche viene denormalizzata o provoca un rallentamento, sono i valori che si avvicinano allo zero ad ogni iterazione del ciclo. Man mano che si avvicinano sempre più allo zero, hanno bisogno di più precisione per rappresentare e diventano denormalizzati. Questi sono i y[i]valori (Si avvicinano a 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'istruzione y[i] = y[i] + 0.1f;. Non appena questa linea viene eseguita ogni iterazione del loop, la precisione extra nel float viene persa e la denormalizzazione necessaria per rappresentare quella precisione non è più necessaria. Successivamente, le operazioni in virgola mobile y[i]rimangono veloci perché non sono denormalizzate.

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

In breve, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;non è la no-op che potresti pensare che sia.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.