Pourquoi ce morceau de code,
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; // <--
}
}
exécuter plus de 10 fois plus rapide que le bit suivant (identique sauf indication contraire)?
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; // <--
}
}
lors de la compilation avec Visual Studio 2010 SP1. (Je n'ai pas testé avec d'autres compilateurs.)
Bienvenue dans le monde de virgule flottante dénormalisée ! Ils peuvent faire des ravages sur les performances !!!
Les nombres dénormaux (ou sous-normaux) sont une sorte de hack pour obtenir des valeurs supplémentaires très proches de zéro en dehors de la représentation en virgule flottante. Les opérations sur une virgule flottante dénormalisée peuvent être des dizaines à des centaines de fois plus lentes que sur une 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'elles ont convergé vers des valeurs différentes selon que vous utilisiez 0
ou 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;
}
Sortie:
#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 que dans la deuxième manche, les nombres sont très proches de zéro.
Les nombres dénormalisés sont généralement rares et la plupart des processeurs n'essayent donc pas de les gérer efficacement.
Pour démontrer que cela a tout à voir avec les nombres dénormalisés, si nous effaçons les dénormaux à zéro en ajoutant ceci au début du code:
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
Ensuite, la version avec 0
ne ralentit plus 10 fois et devient plus rapide. (Cela nécessite que le code soit compilé avec SSE activé.)
Cela signifie qu'au lieu d'utiliser ces valeurs étranges de précision inférieure presque nulle, nous arrondissons simplement à zéro.
Synchronisations: 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 s'il s'agit d'un entier ou d'une virgule flottante. Le 0
ou 0.1f
est converti/enregistré dans un registre situé en dehors des deux boucles. Cela n'a donc aucun effet sur les performances.
Utiliser gcc
et appliquer un diff à l'assembly généré ne produit que cette différence:
73c68,69
< movss LCPI1_0(%rip), %xmm1
---
> movabsq $0, %rcx
> cvtsi2ssq %rcx, %xmm1
81d76
< subss %xmm1, %xmm0
Le cvtsi2ssq
est 10 fois plus lent.
Apparemment, la version float
utilise un registre XMM chargé de la mémoire, tandis que la version int
convertit une valeur réelle int
de 0 à float
en utilisant l'instruction cvtsi2ssq
, prenant beaucoup de temps. Passer -O3
à gcc n'aide pas. (gcc version 4.2.1.)
(Utiliser double
au lieu de float
n'a pas d'importance, sauf que cela change le cvtsi2ssq
en cvtsi2sdq
.)
Mettre à jour
Certains tests supplémentaires montrent qu'il ne s'agit pas nécessairement de l'instruction cvtsi2ssq
. Une fois éliminé (en utilisant un int ai=0;float a=ai;
et en utilisant a
à la place de 0
), la différence de vitesse reste. Alors, @Mysticial a raison, les flotteurs dénormalisés font la différence. Ceci peut être constaté en testant des valeurs entre 0
et 0.1f
. Le point tournant dans le code ci-dessus est approximativement à 0.00000000000000000000000000000001
, lorsque les boucles prennent 10 fois plus de temps.
Mise à jour << 1
Une petite visualisation de ce phénomène intéressant:
Vous pouvez clairement voir l'exposant (les 9 derniers bits) passer à sa valeur la plus basse lorsque la dénormalisation est activée. À ce stade, l'addition simple devient 20 fois plus lente.
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
Une discussion équivalente sur ARM peut être trouvée dans la question Débordement de pile virgule flottante dénormalisée dans Objective-C?.
Cela est dû à une utilisation en virgule flottante dénormalisée. Comment se débarrasser à la fois de cette pénalité et de la performance? Après avoir parcouru Internet à la recherche des moyens de supprimer les nombres dénormaux, il semble qu’il n’existe pas encore de "meilleure" façon de le faire. J'ai trouvé ces trois méthodes qui fonctionnent le mieux dans différents environnements:
Peut ne pas fonctionner dans certains environnements GCC:
// Requires #include <fenv.h>
fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
Peut ne pas fonctionner dans certains environnements 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)
Semble fonctionner à la fois dans GCC et 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);
Le compilateur Intel dispose d’options pour désactiver les valeurs par défaut sur les processeurs Intel modernes. Plus de détails ici
Commutateurs de compilateur. -ffast-math
, -msse
ou -mfpmath=sse
désactive les dénormalités et accélère d'autres tâches, mais fait malheureusement beaucoup d'autres approximations susceptibles de casser votre code. Testez soigneusement! L'équivalent de fast-math pour le compilateur Visual Studio est /fp:fast
, mais je n'ai pas été en mesure de confirmer si cela désactive également les annotations . 1
Dans gcc, vous pouvez activer les zones franches et les zones DAZ avec ceci:
#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);
}
utilisez également les commutateurs gcc: -msse -mfpmath = sse
(crédits correspondants à Carl Hetherington [1])
commentaire de Dan Neely devrait être développé en réponse:
Ce n'est pas la constante zéro 0.0f
qui est dénormalisée ou provoque un ralentissement, ce sont les valeurs qui s'approchent de zéro à chaque itération de la boucle. À mesure qu'ils se rapprochent de plus en plus de zéro, ils ont besoin de plus de précision pour être représentés et ils deviennent dénormalisés. Ce sont les valeurs y[i]
. (Ils s'approchent de zéro car x[i]/z[i]
est inférieur à 1.0 pour tout i
.)
La différence cruciale entre les versions lente et rapide du code est l'instruction y[i] = y[i] + 0.1f;
. Dès que cette ligne est exécutée à chaque itération de la boucle, la précision supplémentaire dans le float est perdue et la dénormalisation nécessaire pour représenter cette précision n'est plus nécessaire. Ensuite, les opérations en virgule flottante sur y[i]
restent rapides car elles ne sont pas dénormalisées.
Pourquoi la précision supplémentaire est-elle perdue lorsque vous ajoutez 0.1f
? Parce que les nombres à virgule flottante ont seulement beaucoup de chiffres significatifs. Supposons que vous avez assez de mémoire pour trois chiffres significatifs, puis 0.00001 = 1e-5
et 0.00001 + 0.1 = 0.1
, du moins pour cet exemple de format flottant, car il ne dispose pas de la place nécessaire pour stocker le bit le moins significatif dans 0.10001
.
En bref, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
n'est pas le no-op que vous pourriez penser.
Mystical a également dit cela : le contenu des flotteurs est important, pas seulement le code d'assemblage.