1626 votes

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

Pourquoi ce bout 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; // <--
    }
}

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

2 votes

Assurez-vous que vous construisez un build de version, pas de débogage.

11 votes

Comment avez-vous mesuré la différence ? Et quelles options avez-vous utilisées lors de la compilation ?

166 votes

Pourquoi le compilateur ne laisse-t-il pas tomber le +/- 0 dans ce cas ? !?

1689voto

Mysticial Points 180300

Bienvenue dans le monde de virgule flottante dénormalisée ! Ils peuvent faire des ravages sur les performances ! !!

Les nombres dénormaux (ou subnormaux) sont une sorte d'astuce pour obtenir des valeurs supplémentaires très proches de zéro à partir de la représentation en virgule flottante. Les opérations sur les nombres à virgule flottante dénormalisés peuvent être des dizaines ou des centaines de fois plus lent que sur la virgule flottante normalisée. En effet, de nombreux processeurs ne peuvent pas les gérer directement et doivent les piéger et les résoudre à l'aide d'un microcode.

Si vous imprimez les nombres après 10 000 itérations, vous verrez qu'ils ont convergé vers des valeurs différentes selon que 0 o 0.1 est utilisé.

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 comment dans la deuxième série, les chiffres sont très proches de zéro.

Les nombres dénormalisés sont généralement rares et la plupart des processeurs n'essaient donc pas de les traiter efficacement.


Pour démontrer que cela a tout à voir avec les nombres dénormalisés, si nous Remettre 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 n'est plus 10x plus lent et devient même plus rapide. (Cela nécessite que le code soit compilé avec SSE activé).

Cela signifie que, plutôt que d'utiliser ces étranges valeurs de précision inférieure presque nulles, nous arrondissons simplement à zéro.

Horaires : 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 rien à voir avec le fait qu'il s'agisse d'un nombre entier ou à virgule flottante. Le site 0 o 0.1f est converti/stocké dans un registre en dehors des deux boucles. Cela n'a donc aucun effet sur les performances.

0 votes

La seule différence dans le code généré est fld qword ptr [__real@3fb99999a0000000 (11820E8h)] pour la version rapide et fldz au début de la boucle d'ailleurs, au moins pour ma construction dans VS2010

4 votes

Dervall C'est exact. Il n'y a presque aucune différence dans le code. C'est la valeur qui affecte le fait que les chiffres deviennent dénormaux ou non.

5 votes

C'est particulièrement intéressant, car la séquence qui change les choses est fondamentalement x += 0.1; x -= 0.1 qui peut aussi s'écrire (x + 0.1) - 0.1 . On note l'absence d'associativité. (Bien sûr, une telle réécriture pourrait changer les résultats, du fait que le C++ permet de conserver les résultats intermédiaires en précision étendue).

425voto

mvds Points 26475

Utilisation de gcc et l'application d'un diff à l'assemblage généré ne donne que cette différence :

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0

El cvtsi2ssq l'un étant 10 fois plus lent en effet.

Apparemment, le float utilise un XMM chargé depuis la mémoire, tandis que le int convertit une version réelle int valeur 0 à float en utilisant le cvtsi2ssq instruction, ce qui prend beaucoup de temps. Passage de -O3 à gcc n'aide pas. (gcc version 4.2.1.)

(En utilisant double au lieu de float n'a pas d'importance, sauf qu'elle change le cvtsi2ssq en un cvtsi2sdq .)

Mise à jour

Certains tests supplémentaires montrent que ce n'est pas forcément le cas. cvtsi2ssq instruction. Une fois éliminé (à l'aide d'un int ai=0;float a=ai; et en utilisant a au lieu de 0 ), la différence de vitesse demeure. Donc @Mysticial a raison, les flottants dénormalisés font la différence. On peut le constater en testant des valeurs comprises entre 0 y 0.1f . Le point d'inflexion dans le code ci-dessus est approximativement à 0.00000000000000000000000000000001 quand les boucles prennent soudainement 10 fois plus de temps.

Mise à jour<<1

Une petite visualisation de ce phénomène intéressant :

  • Colonne 1 : un float, divisé par 2 pour chaque itération
  • Colonne 2 : la représentation binaire de ce flotteur
  • Colonne 3 : le temps nécessaire 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 moment-là, la simple addition 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 Stack Overflow Virgule flottante dénormalisée en Objective-C ? .

28 votes

-O n'arrangent pas les choses, mais -ffast-math fait. (Je l'utilise tout le temps, l'OMI les cas particuliers où il cause des problèmes de précision ne devraient pas se présenter dans un programme correctement conçu de toute façon).

0 votes

Il n'y a pas de conversion à un quelconque niveau d'optimisation positive avec gcc-4.6.

0 votes

@leftaroundabout : compiler un exécutable (pas une bibliothèque) avec -ffast-math lie un code de démarrage supplémentaire qui définit FTZ (flush to zero) et DAZ (denormal are zero) dans le MXCSR, de sorte que le CPU n'a jamais à prendre une assistance microcode lente pour les denormaux.

39voto

fig Points 147

C'est dû à l'utilisation de la virgule flottante dénormalisée. Comment se débarrasser à la fois de ce problème et de la pénalité de performance ? Après avoir parcouru l'Internet pour trouver des moyens de tuer les nombres dénormalisés, 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 au mieux dans des environnements différents :

  • 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 a des options pour désactiver les dénormaux par défaut sur les CPU Intel modernes. Plus de détails ici

  • Commutateurs de compilateur. -ffast-math , -msse o -mfpmath=sse désactivera les dénormaux et rendra quelques autres choses plus rapides, mais malheureusement fera aussi 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 été en mesure de confirmer si cela désactive également les dénormaux. 1

4 votes

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.

1 votes

Windows X64 passe un paramètre d'abrupt underflow quand il lance .exe, alors que Windows 32-bit et linux ne le font pas. Sous linux, gcc -ffast-math devrait activer le paramètre abrupt underflow (mais je pense que non sous Windows). Les compilateurs Intel sont censés initialiser dans main() pour que ces différences d'OS ne passent pas, mais j'ai été mordu, et j'ai besoin de le régler explicitement dans le programme. Les processeurs Intel à partir de Sandy Bridge sont censés gérer efficacement les sous-normes survenant dans l'addition/soustraction (mais pas dans la division/multiplication), donc il y a un cas pour utiliser l'underflow progressif.

2 votes

Microsoft /fp:fast (qui n'est pas un 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.

20voto

remicles2 Points 131

Commentaire de Dan Neely doit être développée dans une réponse :

Ce n'est pas la constante zéro 0.0f qui est dénormalisée ou qui provoque un ralentissement, ce sont les valeurs qui s'approchent de zéro à chaque itération de la boucle. Comme elles se rapprochent de plus en plus de zéro, elles ont besoin de plus de précision pour être représentées et elles sont dénormalisées. Ce sont les valeurs y[i] valeurs. (Elles s'approchent de zéro parce que x[i]/z[i] est inférieure à 1,0 pour toutes les 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 du flottant 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 n'ont qu'un nombre limité de chiffres significatifs. Si vous avez assez de place pour trois chiffres significatifs, alors 0.00001 = 1e-5 et 0.00001 + 0.1 = 0.1 du moins pour cet exemple de format flottant, parce qu'il n'y a pas de place pour stocker le bit le moins significatif dans le code de l'erreur. 0.10001 .

En bref, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; n'est pas aussi inutile qu'on pourrait le croire.

Mystique a également dit ceci Le contenu des flottants est important, pas seulement le code d'assemblage.

EDIT : Pour être plus précis, toutes les opérations en virgule flottante ne prennent pas le même temps d'exécution, même si l'opcode de la machine est le même. Pour certains opérandes/entrées, la même instruction prendra plus de temps à s'exécuter. Ceci est particulièrement vrai pour les nombres dénormaux.

20voto

German Garcia Points 702

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

0 votes

Voir aussi fesetround() de fenv.h (défini pour C99) pour une autre méthode d'arrondi, plus portable ( linux.die.net/man/3/fesetround ) (mais cette affecterait toutes les opérations FP, pas seulement les subnormales )

0 votes

Etes-vous sûr que vous avez besoin de 1<<15 et 1<<11 pour FTZ ? Je n'ai vu que 1<<15 cité ailleurs...

0 votes

@fig : 1<<11 est pour le masque d'underflow. Plus d'informations ici : softpixel.com/~cwright/programmation/simd/sse.php

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X