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; // <--
}
}
fonctionner plus de 10 fois plus vite 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. Le niveau d'optimisation a été -02
avec sse2
permis. Je n'ai pas testé avec d'autres compilateurs.
0
,0f
,0d
, ou même(int)0
dans un contexte oùdouble
est nécessaire.Réponses:
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 non0.1
.Voici le code de test compilé sur x64:
Production:
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:
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:
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
ou0.1f
est converti / stocké dans un registre en dehors des deux boucles. Cela n'a donc aucun effet sur les performances.la source
+ 0.0f
optimisé. Si je devais deviner, il se pourrait que+ 0.0f
cela ait des effets secondaires s'ily[i]
s'avérait être une signalisationNaN
ou quelque chose ... Je peux me tromper cependant.L'utilisation
gcc
et l'application d'un diff à l'assembly généré ne produit que cette différence:le
cvtsi2ssq
étant 10 fois plus lent en effet.Apparemment, la
float
version utilise un registre XMM chargé à partir de la mémoire, tandis que laint
version convertit uneint
valeur réelle 0 enfloat
utilisant l'cvtsi2ssq
instruction, ce qui prend beaucoup de temps. Qui passe-O3
à gcc n'aide pas. (version gcc 4.2.1.)(Utiliser
double
au lieu defloat
n'a pas d'importance, sauf qu'il change lecvtsi2ssq
en acvtsi2sdq
.)Mise à jour
Certains tests supplémentaires montrent que ce n'est pas nécessairement l'
cvtsi2ssq
instruction. Une fois éliminée (en utilisant aint ai=0;float a=ai;
et en utilisanta
au lieu de0
), la différence de vitesse reste. @Mysticial a donc raison, les flotteurs dénormalisés font la différence. Cela peut être vu en testant les valeurs entre0
et0.1f
. Le point tournant dans le code ci-dessus est approximativement à0.00000000000000000000000000000001
, lorsque les boucles prennent soudainement 10 fois plus de temps.Mettre à 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 s'installe. À ce stade, l'addition simple devient 20 fois plus lente.
Une discussion équivalente sur ARM peut être trouvée dans la question de débordement de pile Virgule flottante dénormalisée dans Objective-C? .
la source
-O
s ne le répare pas, mais le-ffast-math
fait. (J'utilise cela tout le temps, l'OMI les cas d'angle où cela cause des problèmes de précision ne devraient de toute façon pas apparaître dans un programme correctement conçu.)-ffast-math
liens un code de démarrage supplémentaire qui définit FTZ (vidage à zéro) et DAZ (dénormal à zéro) dans le MXCSR, de sorte que le CPU n'a jamais à prendre une assistance de microcode lente pour les dénormals.Cela est dû à une utilisation en virgule flottante dénormalisée. Comment se débarrasser de cela et de la pénalité de performance? Ayant parcouru Internet pour trouver des moyens de tuer les nombres dénormaux, il semble qu'il n'y ait pas encore de "meilleure" façon de le faire. J'ai trouvé ces trois méthodes qui peuvent fonctionner le mieux dans différents environnements:
Pourrait ne pas fonctionner dans certains environnements GCC:
Peut ne pas fonctionner dans certains environnements Visual Studio: 1
Semble fonctionner à la fois dans GCC et Visual Studio:
Le compilateur Intel a des options pour désactiver les dénormals par défaut sur les processeurs Intel modernes. Plus de détails ici
Commutateurs du compilateur.
-ffast-math
,-msse
ou-mfpmath=sse
désactivera les dénormals et accélérera quelques autres choses, mais malheureusement, fera également beaucoup d'autres approximations qui pourraient casser votre code. Testez soigneusement! L'équivalent de fast-math pour le compilateur Visual Studio est/fp:fast
mais je n'ai pas pu confirmer si cela désactive également les dénormals. 1la source
Dans gcc, vous pouvez activer FTZ et DAZ avec ceci:
utilisez également les commutateurs gcc: -msse -mfpmath = sse
(crédits correspondants à Carl Hetherington [1])
[1] http://carlh.net/plugins/denormals.php
la source
fesetround()
partir defenv.h
(défini pour C99) pour un autre moyen d'arrondi plus portable ( linux.die.net/man/3/fesetround ) (mais cela affecterait toutes les opérations de FP, pas seulement les subnormales )Le commentaire de Dan Neely devrait être développé en une 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 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 représenter et ils deviennent dénormalisés. Ce sont lesy[i]
valeurs. (Ils approchent de zéro carx[i]/z[i]
est inférieur à 1,0 pour tousi
.)La différence cruciale entre les versions lente et rapide du code est la déclaration
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 flottant est perdue et la dénormalisation nécessaire pour représenter cette précision n'est plus nécessaire. Par la suite, les opérations en virgule flottantey[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 autant de chiffres significatifs. Supposons que vous ayez suffisamment de stockage pour trois chiffres significatifs0.00001 = 1e-5
, et0.00001 + 0.1 = 0.1
, au moins pour cet exemple de format flottant, car il n'a pas de place pour stocker le bit le moins significatif0.10001
.En bref, ce
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
n'est pas le non-op que vous pourriez penser.Mystical l'a également dit : le contenu des flotteurs est important, pas seulement le code d'assemblage.
la source