Bienvenue dans le monde de la virgule flottante dénormalisée ! Ils peuvent faire des ravages sur les performances !!!
Les nombres dénormaux (ou subnormaux) sont une sorte de hack pour obtenir des valeurs supplémentaires très proches de zéro de la représentation en virgule flottante. Les opérations sur virgule flottante dénormalisée peuvent être des dizaines à des centaines de fois plus lentes que sur virgule flottante normalisée. En effet, de nombreux processeurs ne peuvent pas les gérer directement et doivent les intercepter et les résoudre à l'aide du microcode.
Si vous imprimez les nombres après 10 000 itérations, vous verrez qu'ils ont convergé vers des valeurs différentes selon qu'ils sont utilisés 0
ou non 0.1
.
Voici le code de test compilé sur 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;
}
Production:
#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
Notez comment, lors de la deuxième exécution, les nombres sont très proches de zéro.
Les nombres dénormalisés sont généralement rares et donc la plupart des processeurs n'essaient pas de les gérer efficacement.
Pour démontrer que cela a tout à voir avec les nombres dénormalisés, si nous vidons les dénormals à zéro en ajoutant ceci au début du code:
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
Ensuite, la version avec 0
n'est plus 10 fois plus lente et devient en fait plus rapide. (Cela nécessite que le code soit compilé avec SSE activé.)
Cela signifie que plutôt que d'utiliser ces valeurs presque nulles de précision inférieure étranges, nous arrondissons simplement à zéro à la place.
Timings: 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
En fin de compte, cela n'a vraiment rien à voir avec le fait qu'il s'agisse d'un entier ou d'une virgule flottante. Le 0
ou 0.1f
est converti / stocké dans un registre en dehors des deux boucles. Cela n'a donc aucun effet sur les performances.