Dlaczego zmiana 0.1 f na 0 spowalnia wydajność o 10x?

Dlaczego ten bit kodu,

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

Uruchomić ponad 10 razy szybciej niż poniższy bit(identyczny, chyba że zaznaczono)?

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

Podczas kompilacji z Visual Studio 2010 SP1. (Nie testowałem z innymi kompilatorami.)

Author: Peter Mortensen, 2012-02-16

5 answers

Witamy w świecie denormalizowanego zmiennoprzecinkowego! mogą siać spustoszenie w wydajności!!!

Liczby Denormalne (lub subnormalne) są rodzajem hakowania, aby uzyskać dodatkowe wartości bardzo bliskie zeru z reprezentacji zmiennoprzecinkowej. Operacje na denormalizowanej zmiennoprzecinkowej mogą być dziesiątki do setek razy wolniej niż na znormalizowanej zmiennoprzecinkowej. Dzieje się tak dlatego, że wiele procesorów nie może obsłużyć ich bezpośrednio i musi uwięzić i rozwiązać używają mikrokodu.

Jeśli wydrukujesz liczby po 10 000 iteracji, zobaczysz, że są one zbieżne do różnych wartości w zależności od tego, czy 0 lub 0.1 jest używany.

Oto kod testowy skompilowany na 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;
}

Wyjście:

#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

Zauważ, że w drugim biegu liczby są bardzo bliskie zeru.

Liczby Denormalizowane są na ogół rzadkie i dlatego większość procesorów nie stara się ich efektywnie obsłużyć.


To wykazanie, że ma to wszystko wspólnego z liczbami denormalizowanymi, jeśli wyrównamy denormalne do zera dodając to do początku kodu:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Wtedy wersja z 0 nie jest już 10x wolniejsza i faktycznie staje się szybsza. (Wymaga to kompilacji kodu z włączoną obsługą SSE.)

Oznacza to, że zamiast używać tych dziwnych, niższych dokładności, prawie zerowych wartości, po prostu zaokrąglamy do zera.

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

W końcu, to naprawdę nie ma nic wspólnego z tym, czy jest to liczba całkowita czy zmiennoprzecinkowa. 0 lub 0.1f jest konwertowany/przechowywany w rejestrze poza obiema pętlami. Więc to nie ma wpływu na wydajność.

 1483
Author: Mysticial,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-17 01:19:26

Użycie gcc i zastosowanie różnicy do wygenerowanego zestawu daje tylko tę różnicę:

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0
Jeden jest 10 razy wolniejszy.

Wygląda na to, że wersja floatużywa rejestru XMM załadowanego z pamięci, podczas gdy wersja int konwertuje rzeczywistą int wartość 0 Na float za pomocą instrukcji cvtsi2ssq, co zajmuje dużo czasu. Podanie -O3 do gcc nie pomaga. (gcc wersja 4.2.1.)

(użycie double zamiast float nie ma znaczenia, poza tym, że zmienia cvtsi2ssq na cvtsi2sdq.)

Update

Niektóre dodatkowe testy pokazują, że niekoniecznie jest to Instrukcja cvtsi2ssq. Po wyeliminowaniu (używając int ai=0;float a=ai; i używając a zamiast 0) różnica prędkości pozostaje. @ Mysticial ma rację, denormalizowane pływaki robią różnicę. Można to zobaczyć testując wartości między 0 i 0.1f. Punkt zwrotny w powyższym kodzie wynosi w przybliżeniu 0.00000000000000000000000000000001, kiedy pętle nagle zajmują 10 razy długa.

Aktualizacja

Mała wizualizacja tego ciekawego zjawiska:

  • Kolumna 1: float, podzielony przez 2 dla każdej iteracji
  • kolumna 2: binarna reprezentacja tego float
  • Column 3: The time taken to sum this float 1e7 times

Widać wyraźnie, że wykładnik (ostatnie 9 bitów) zmienia się na najniższą wartość, gdy denormalizacja zostanie ustawiona. W tym momencie proste dodawanie staje się 20 razy wolniej.

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

Równoważną dyskusję na temat ARM można znaleźć w pytaniu o przepełnienie stosu Denormalizowana zmiennoprzecinkowa w Objective-C?.

 399
Author: mvds,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:23

Jest to spowodowane denormalizowanym użyciem zmiennoprzecinkowym. Jak pozbyć się zarówno tego, jak i kary za wykonanie? Po przeszukaniu Internetu w poszukiwaniu sposobów zabijania liczb denormalnych, wydaje się, że nie ma jeszcze "najlepszego" sposobu, aby to zrobić. Znalazłem te trzy metody, które mogą działać najlepiej w różnych środowiskach: {]}

  • Może nie działać w niektórych środowiskach GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Może nie działać w niektórych środowiskach 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)
    
  • Wygląda na to, że działa zarówno w GCC, jak i 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);
    
  • Kompilator Intela ma domyślnie opcje wyłączania denormaliów na nowoczesnych procesorach Intela. więcej szczegółów tutaj

  • Przełączniki kompilatora. -ffast-math, -msse or -mfpmath=sse wyłączy denormals i zrobić kilka innych rzeczy szybciej, ale niestety również zrobić wiele innych przybliżeń, które mogą złamać kod. Testuj ostrożnie! Odpowiednik fast-math dla Kompilator Visual Studio jest /fp:fast, ale nie byłem w stanie potwierdzić, czy to również wyłącza denormale.1

 29
Author: fig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-23 15:53:53

W gcc możesz włączyć FTZ i DAZ za pomocą tego:

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

Użyj również przełączników gcc: - msse-mfpmath=sse

(odpowiadające napisy dla Carla Hetheringtona [1])

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

 19
Author: German Garcia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-02 04:40:26

Komentarz dana Neely ' ego powinien zostać rozszerzony na odpowiedź:

To nie jest stała zerowa 0.0f, która jest denormalizowana lub powoduje spowolnienie, to są wartości, które zbliżają się do zera każdej iteracji pętli. Gdy zbliżają się coraz bliżej do zera, potrzebują większej precyzji do reprezentowania i stają się denormalizowane. Są to wartości y[i]. (Zbliżają się do zera, ponieważ x[i]/z[i] jest mniejsze niż 1,0 dla wszystkich i.)

Zasadnicza różnica pomiędzy powolnym i szybkie wersje kodu to polecenie y[i] = y[i] + 0.1f;. Gdy tylko ta linia jest wykonywana przy każdej iteracji pętli, dodatkowa precyzja w float jest tracona, a denormalizacja potrzebna do reprezentowania tej precyzji nie jest już potrzebna. Następnie operacje zmiennoprzecinkowe na y[i] pozostają szybkie, ponieważ nie są denormalizowane.

Dlaczego dodatkowa precyzja jest tracona po dodaniu 0.1f? Ponieważ liczby zmiennoprzecinkowe mają tylko tyle znaczących cyfr. Powiedz, że masz wystarczająco dużo miejsca na trzy znaczące cyfry, następnie 0.00001 = 1e-5 i 0.00001 + 0.1 = 0.1, przynajmniej dla tego przykładowego formatu float, ponieważ nie ma miejsca na przechowywanie najmniej znaczącego bitu w 0.10001.

W skrócie, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; nie jest no-op można myśleć, że jest.

Mistyczny powiedział to również: liczy się treść pływaków, a nie tylko kod montażowy.

 2
Author: remicles2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-01 13:53:29