Witamy w świecie zdominowanych zmiennoprzecinkowych ! Mogą siać spustoszenie w wydajności!
Liczby normalne (lub subnormalne) są rodzajem hackowania, aby uzyskać dodatkowe wartości bardzo zbliżone do zera z reprezentacji zmiennoprzecinkowej. Operacje na znormalizowanym zmiennoprzecinkowym mogą być dziesiątki do setek razy wolniejsze niż na znormalizowanym zmiennoprzecinkowym. Wynika to z faktu, że wiele procesorów nie radzi sobie z nimi bezpośrednio i musi je przechwycić i rozwiązać za pomocą mikrokodu.
Jeśli wydrukujesz liczby po 10 000 iteracjach, zobaczysz, że zbiegły się one w różne wartości w zależności od tego, 0
czy 0.1
są używane.
Oto kod testowy skompilowany na 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;
}
Wynik:
#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
Zwróć uwagę, że w drugim przebiegu liczby są bardzo bliskie zeru.
Numery zdormalizowane są na ogół rzadkie, dlatego większość procesorów nie próbuje ich obsługiwać skutecznie.
Aby zademonstrować, że ma to wszystko wspólnego z liczbami zdenormalizowanymi, jeśli wyzerujemy wartości normalne do zera , dodając to na początku kodu:
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
Wtedy wersja z 0
nie jest już 10 razy wolniejsza i faktycznie staje się szybsza. (Wymaga to kompilacji kodu z włączoną obsługą SSE).
Oznacza to, że zamiast używać tych dziwnych, prawie zerowych wartości o mniejszej precyzji, po prostu zaokrąglamy do zera.
Czasy pracy: 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
Ostatecznie nie ma to nic wspólnego z liczbą całkowitą czy zmiennoprzecinkową. Symbol 0
lub 0.1f
jest konwertowany / przechowywany w rejestrze poza obiema pętlami. To nie ma wpływu na wydajność.