Pourquoi le changement de 0.1 f à 0 ralentit-il la performance de 10x?

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; // <--
    }
}

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

1373
demandé sur Peter Mortensen 2012-02-16 19:58:39
la source

5 ответов

Bienvenue dans le monde de dénormalisée virgule flottante ! ils peuvent faire des ravages sur la performance!!!

les nombres Dénormaux (ou sous-normaux) 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 point flottant dénormalisé peut être des dizaines à des centaines de fois plus lent que sur normalisé à virgule flottante. C'est parce que beaucoup de processeurs ne peuvent pas les manipuler directement et doivent les piéger et les résoudre en utilisant le 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 ou 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 exécution les nombres sont très proches de zéro.

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


Pour démontrer que cela a tout à voir avec les nombres dénormalisés, si nous flush denormals à zéro en ajoutant ceci au début du code:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

puis la version avec 0 n'est plus 10x plus lent 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, nous tournons à zéro à la place.

minuteries: 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'un point flottant. Le 0 ou 0.1f est converti/stocké dans un registre à l'extérieur des deux boucles. Donc cela n'a aucun effet sur les performances.

1483
répondu Mysticial 2012-02-17 05:19:26
la source

en utilisant gcc et en appliquant une différence à l'assemblage généré ne donne que cette différence:

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

le cvtsi2ssq un étant 10 fois plus lent en effet.

apparemment, la version float utilise un XMM registre chargé de mémoire, tandis que la int version convertit une vraie int valeur 0 à float en utilisant l'instruction cvtsi2ssq , ce qui prend beaucoup de temps. Passant -O3 pour gcc n'aide pas. (version de gcc 4.2.1.)

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

mise à Jour

certains essais 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 au lieu de 0 ), la vitesse la différence reste. Donc @Mysticial a raison, les flotteurs dénormalisés font la différence. On peut le voir en testant les valeurs entre 0 et 0.1f . Le point de virage dans le code ci-dessus est approximativement à 0.00000000000000000000000000000001 , quand les boucles prend soudainement 10 fois plus de temps.

mise à Jour<<1

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

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

vous pouvez clairement voir l'exposant (les 9 derniers bits) changer à sa valeur la plus basse, lorsque la dénormalisation s'installe. À ce moment, 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 sur le débordement des cheminées point flottant dénormalisé dans L'objectif-c? .

399
répondu mvds 2017-05-23 15:18:23
la source

c'est dû à l'utilisation dénormalisée du flottant. Comment se débarrasser à la fois de lui et de la pénalité de performance? Après avoir cherché sur Internet des moyens de tuer des nombres anormaux, il semble qu'il n'y ait pas encore de "meilleure" façon de le faire. J'ai trouvé ces trois méthodes peuvent mieux travailler dans des environnements différents:

  • pourrait ne pas fonctionner dans certains environnements de GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • pourrait ne pas fonctionner dans certains Environnements de studio visuel: 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 dans les deux 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 processeurs Intel modernes. plus de détails ici

  • commutateurs de compilateurs. -ffast-math , -msse ou -mfpmath=sse désactiver denormals et faire quelques autres choses plus rapidement, mais malheureusement aussi faire beaucoup d'autres approximations qui pourraient briser 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 denormaux. 1

29
répondu fig 2014-07-23 19:53:53
la source

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);
}

utiliser également des commutateurs gcc: - msse-mfpmath=sse

(crédits correspondants à Carl Hetherington [1])

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

19
répondu German Garcia 2012-10-02 08:40:26
la source

Dan Neely commentaire devrait être élargie de répondre à la question:

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 zéro chaque itération de la boucle. Au fur et à mesure qu'ils se rapprochent de zéro, ils ont besoin de plus de précision pour représenter et ils deviennent dénormalisés. Ce sont les valeurs y[i] . (Ils approchent de zéro parce que x[i]/z[i] est inférieur à 1,0 pour tous i .)

la différence cruciale entre la version lente et la version rapide du code est la mention 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 flotteur 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 flottantes sur y[i] restent rapides car elles ne sont pas dénormalisées.

pourquoi la précision supplémentaire est-elle perdue quand vous ajoutez 0.1f ? Parce que les nombres à virgule flottante ont seulement autant de chiffres significatifs. Disons que vous avez assez de stockage pour trois chiffres significatifs, puis 0.00001 = 1e-5 , et 0.00001 + 0.1 = 0.1 , au moins pour cet exemple de format float, parce qu'il n'a pas de place pour stocker le bit le moins significatif dans 0.10001 .

en résumé, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; n'est pas le no-op que vous pourriez penser qu'il est.

Mystical dit aussi : le le contenu des flotteurs est important, pas seulement le code de l'Assemblée.

2
répondu remicles2 2018-08-01 16:53:29
la source