Pourquoi le passage de 0,1f à 0 ralentit-il 10 fois les performances?

1528

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é -02avec sse2permis. Je n'ai pas testé avec d'autres compilateurs.

Dragarro
la source
10
Comment avez-vous mesuré la différence? Et quelles options avez-vous utilisées lors de la compilation?
James Kanze
158
Pourquoi le compilateur ne laisse-t-il pas simplement tomber le +/- 0 dans ce cas?!?
Michael Dorgan
127
@ Zyx2000 Le compilateur est loin d'être aussi stupide. Un exemple trivial démonter dans des spectacles LINQPad qu'il recrache le même code que vous utilisez 0, 0f, 0d, ou même (int)0dans un contexte où doubleest nécessaire.
millimoose
14
quel est le niveau d'optimisation?
Otto Allmendinger

Réponses:

1617

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 0ou 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 0n'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 0ou 0.1fest converti / stocké dans un registre en dehors des deux boucles. Cela n'a donc aucun effet sur les performances.

Mysticial
la source
100
Je trouve toujours un peu bizarre que le "+ 0" ne soit pas complètement optimisé par le compilateur par défaut. Cela serait-il arrivé s'il avait mis "+ 0,0f"?
s73v3r
51
@ s73v3r C'est une très bonne question. Maintenant que je regarde l'assemblage, même pas + 0.0foptimisé. Si je devais deviner, il se pourrait que + 0.0fcela ait des effets secondaires s'il y[i]s'avérait être une signalisation NaNou quelque chose ... Je peux me tromper cependant.
Mysticial
14
Les doubles rencontreront toujours le même problème dans de nombreux cas, juste à une amplitude numérique différente. Flush-to-zero est très bien pour les applications audio (et d'autres où vous pouvez vous permettre de perdre 1e-38 ici et là), mais je crois que cela ne s'applique pas à x87. Sans FTZ, la solution habituelle pour les applications audio consiste à injecter un signal CC ou carré ou à onde carrée de très faible amplitude (non audible) pour écarter les nombres de la dénormalité.
Russell Borogove
16
@Isaac car lorsque y [i] est significativement inférieur à 0,1, l'ajout entraîne une perte de précision car le chiffre le plus significatif du nombre devient plus élevé.
Dan Fiddling By Firelight
167
@ s73v3r: le + 0.f ne peut pas être optimisé car la virgule flottante a un 0 négatif et le résultat de l'ajout de + 0.f à -.0f est + 0.f. L'ajout de 0.f n'est donc pas une opération d'identité et ne peut pas être optimisé.
Eric Postpischil
415

L'utilisation gccet l'application d'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 étant 10 fois plus lent en effet.

Apparemment, la floatversion utilise un registre XMM chargé à partir de la mémoire, tandis que la intversion convertit une intvaleur réelle 0 en floatutilisant l' cvtsi2ssqinstruction, ce qui prend beaucoup de temps. Qui passe-O3 à gcc n'aide pas. (version gcc 4.2.1.)

(Utiliser doubleau lieu de floatn'a pas d'importance, sauf qu'il change le cvtsi2ssqen a cvtsi2sdq.)

Mise à jour

Certains tests supplémentaires montrent que ce n'est pas nécessairement l' cvtsi2ssqinstruction. Une fois éliminée (en utilisant a int ai=0;float a=ai;et en utilisant aau lieu de 0), 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 entre 0et 0.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:

  • Colonne 1: un flotteur, divisé par 2 pour chaque itération
  • Colonne 2: la représentation binaire de ce flottant
  • Colonne 3: le temps mis pour additionner ce flotteur 1e7 fois

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.

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 de débordement de pile Virgule flottante dénormalisée dans Objective-C? .

mvds
la source
27
-Os ne le répare pas, mais le -ffast-mathfait. (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.)
leftaroundabout
Il n'y a pas de conversion à un niveau d'optimisation positif avec gcc-4.6.
Jed
@leftaroundabout: compiler un exécutable (pas une bibliothèque) avec des -ffast-mathliens 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.
Peter Cordes
34

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:

    // 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 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, -msseou -mfpmath=ssedé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:fastmais je n'ai pas pu confirmer si cela désactive également les dénormals. 1

figure
la source
1
Cela ressemble à une réponse décente à une question différente mais connexe (Comment puis-je empêcher les calculs numériques de produire des résultats dénormaux?) Cela ne répond pas à cette question, cependant.
Ben Voigt
Windows X64 passe un paramètre de débordement brutal lors du lancement de .exe, contrairement à Windows 32 bits et linux. Sous linux, gcc -ffast-math devrait définir un underflow brutal (mais je ne pense pas sous Windows). Les compilateurs Intel sont censés s'initialiser dans main () afin que ces différences de système d'exploitation ne passent pas, mais j'ai été mordu et j'ai besoin de le définir explicitement dans le programme. Les processeurs Intel commençant par Sandy Bridge sont censés gérer efficacement les sous-normales résultant de l'ajout / soustraction (mais pas de la division / multiplication), il est donc judicieux d'utiliser un débordement progressif.
tim18
1
Microsoft / fp: fast (pas par défaut) ne fait aucune des choses agressives inhérentes à gcc -ffast-math ou ICL (par défaut) / fp: fast. C'est plus comme ICL / fp: source. Vous devez donc définir / fp: (et, dans certains cas, le mode underflow) explicitement si vous souhaitez comparer ces compilateurs.
tim18
18

Dans gcc, vous pouvez activer FTZ et 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])

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

Garcia allemand
la source
Voir également à fesetround()partir de fenv.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 )
German Garcia
Êtes-vous sûr d'avoir besoin de 1 << 15 et 1 << 11 pour FTZ? Je n'ai vu que 1 << 15 cités ailleurs ...
fig
@fig: 1 << 11 est pour le masque de débordement. Plus d'informations ici: softpixel.com/~cwright/programming/simd/sse.php
German Garcia
@GermanGarcia cela ne répond pas à la question des PO; la question était "Pourquoi ce morceau de code s'exécute-t-il 10 fois plus vite que ..." - vous devriez soit essayer de répondre à cela avant de fournir cette solution de contournement, soit fournir ceci dans un commentaire.
9

Le commentaire de Dan Neely devrait être développé en une réponse:

Ce n'est pas la constante zéro 0.0fqui 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 les y[i]valeurs. (Ils approchent de zéro car x[i]/z[i]est inférieur à 1,0 pour tous i.)

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 flottante 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 autant de chiffres significatifs. Supposons que vous ayez suffisamment de stockage pour trois chiffres significatifs 0.00001 = 1e-5, et 0.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 significatif 0.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.

remcycles
la source