Vraag Waarom vertraagt ​​het veranderen van 0.1f tot 0 de prestaties met 10x?


Waarom doet dit stukje 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; // <--
    }
}

meer dan 10 keer sneller lopen dan het volgende bit (identiek behalve waar aangegeven)?

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

bij het compileren met Visual Studio 2010 SP1. (Ik heb niet getest met andere compilers.)


1359
2018-02-16 15:58


oorsprong


antwoorden:


Welkom in de wereld van gedenormaliseerd drijvend punt! Ze kunnen grote schade aanrichten !!!

Denormale (of subnormale) getallen zijn een soort hack om een ​​aantal extra waarden dicht bij nul te krijgen uit de weergave met drijvende komma. Bewerkingen op gedenormaliseerd drijvend punt kunnen zijn tientallen tot honderden keren langzamer dan op genormaliseerd drijvend punt. Dit komt omdat veel processors ze niet direct kunnen verwerken en ze moeten vangen en oplossen met behulp van microcode.

Als u de getallen na 10.000 iteraties afdrukt, ziet u dat ze naar verschillende waarden zijn geconvergeerd, afhankelijk van of 0 of 0.1 is gebruikt.

Hier is de testcode gecompileerd op 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;
}

Output:

#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

Merk op hoe in de tweede run de cijfers zeer dicht bij nul liggen.

Gedemormaliseerde nummers zijn over het algemeen zeldzaam en daarom proberen de meeste processors ze niet efficiënt te verwerken.


Om aan te tonen dat dit alles te maken heeft met gedenormaliseerde getallen, als we dat doen flush denormals naar nul door dit toe te voegen aan het begin van de code:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Dan is de versie met 0 is niet langer 10x langzamer en wordt eigenlijk sneller. (Dit vereist dat de code wordt gecompileerd met SSE ingeschakeld.)

Dit betekent dat in plaats van deze rare lagere precisie bijna-nulwaarden te gebruiken, we in plaats daarvan gewoon naar nul afronden.

Tijdschema: 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

Uiteindelijk heeft dit niets te maken met of het een geheel getal of een drijvend punt is. De 0 of 0.1f wordt geconverteerd / opgeslagen in een register buiten beide lussen. Dat heeft dus geen effect op de prestaties.


1470
2018-02-16 16:20



Gebruik makend van gcc en het toepassen van een diff op het gegenereerde samenstel levert alleen dit verschil op:

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

De cvtsi2ssq een is inderdaad 10 keer langzamer.

Blijkbaar is de float versie gebruikt een XMM register geladen vanuit geheugen, terwijl de int versie converteert een real int waarde 0 tot float de ... gebruiken cvtsi2ssq instructie, neemt veel tijd in beslag. Passing -O3 to gcc helpt niet. (gcc-versie 4.2.1.)

(Gebruik makend van double in plaats van float doet er niet toe, behalve dat het de cvtsi2ssq in een cvtsi2sdq.)

Bijwerken 

Sommige extra tests tonen aan dat het niet noodzakelijk het cvtsi2ssq instructie. Eenmaal geëlimineerd (met behulp van een int ai=0;float a=ai; en gebruiken a in plaats van 0), het snelheidsverschil blijft. Dus @ Mysticial heeft gelijk, de gedenormaliseerde vlotters maken het verschil. Dit kan worden gezien door waarden tussen te testen 0 en 0.1f. Het keerpunt in de bovenstaande code is ongeveer om 0.00000000000000000000000000000001, wanneer de lussen ineens 10 keer zo lang duren.

Update << 1 

Een kleine visualisatie van dit interessante fenomeen:

  • Kolom 1: een float, gedeeld door 2 voor elke iteratie
  • Kolom 2: de binaire weergave van deze dobber
  • Kolom 3: de tijd die nodig is om deze float de 1e keer te tellen

Je kunt duidelijk zien dat de exponent (de laatste 9 bits) naar de laagste waarde gaat, wanneer denormalisatie optreedt. Op dat moment wordt de eenvoudige toevoeging 20 keer langzamer.

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

Een gelijkwaardige discussie over ARM is te vinden in de Stack Overflow-vraag Denormalized floating point in Objective-C?.


399
2018-02-16 16:19



Het is te wijten aan gedenormaliseerd drijvend puntgebruik. Hoe zich te ontdoen van zowel het als de prestatieboete? Nadat we internet hebben doorzocht op manieren om denormale aantallen te doden, lijkt het erop dat er nog geen "beste" manier is om dit te doen. Ik heb deze drie methoden gevonden die mogelijk het beste werken in verschillende omgevingen:

  • Werkt misschien niet in sommige GCC-omgevingen:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Werkt mogelijk niet in sommige Visual Studio-omgevingen: 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)
    
  • Lijkt te werken in zowel GCC als 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);
    
  • De Intel-compiler heeft opties om denormals standaard uit te schakelen op moderne Intel-CPU's. Meer informatie hier

  • Compilerschakelaars. -ffast-math, -msse of -mfpmath=sse zal denormals uitschakelen en een paar andere dingen sneller doen, maar helaas doen ook veel andere benaderingen die uw code kunnen breken. Test zorgvuldig! Het equivalent van snelle wiskunde voor de Visual Studio-compiler is /fp:fast maar ik heb niet kunnen bevestigen of dit denormalen ook uitschakelt.1


29
2018-02-26 12:15



In gcc kun je FTZ en DAZ hiermee inschakelen:

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

gebruik ook gcc-schakelopties: -msse -mfpmath = sse

(overeenkomstige credits voor Carl Hetherington [1])

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


19
2017-10-02 04:40



Dan Neely's commentaar zou moeten worden uitgebreid tot een antwoord:

Het is niet de constante nul 0.0f dat wordt gedenormaliseerd of veroorzaakt een vertraging, het zijn de waarden die elke iteratie van de lus naderen. Naarmate ze steeds dichter bij nul komen, hebben ze meer precisie nodig om te vertegenwoordigen en worden ze gedenormaliseerd. Dit zijn de y[i] waarden. (Ze benaderen nul omdat x[i]/z[i] is minder dan 1.0 voor iedereen i.)

Het cruciale verschil tussen de langzame en snelle versies van de code is de verklaring y[i] = y[i] + 0.1f;. Zodra deze lijn elke iteratie van de lus heeft uitgevoerd, gaat de extra precisie in de vlotter verloren en is de denormalisatie die nodig is om die precisie weer te geven, niet langer nodig. Naderhand werken drijvende-kommabewerkingen op y[i] blijf snel omdat ze niet worden gedenormaliseerd.

Waarom is de extra precisie verloren als je iets toevoegt? 0.1f? Omdat getallen met drijvende komma alleen zoveel significante cijfers bevatten. Stel dat je voldoende opslagruimte hebt voor drie significante cijfers 0.00001 = 1e-5, en 0.00001 + 0.1 = 0.1, althans voor dit voorbeeld float-formaat, omdat er geen ruimte is om het minst significante bit op te slaan 0.10001.

In het kort, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; is niet de no-op die je misschien denkt dat het is.

Mystical zei dit ook: de inhoud van de drijvers is van belang, niet alleen de assemblagecode.


0
2017-08-01 13:32