Frage Warum wird durch die Änderung von 0,1f auf 0 die Leistung um 10x verringert?


Warum macht dieses Bit 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; // <--
    }
}

run mehr als 10-mal schneller als das folgende Bit (identisch, außer wenn notiert)?

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

beim Kompilieren mit Visual Studio 2010 SP1. (Ich habe nicht mit anderen Compilern getestet.)


1359
2018-02-16 15:58


Ursprung


Antworten:


Willkommen in der Welt von denormalisierter Fließkommawert! Sie können Chaos bei der Performance anrichten !!!

Denormale (oder subnormale) Zahlen sind eine Art Hack, um einige Extralwerte nahe Null aus der Gleitkommadarstellung zu erhalten. Operationen auf demormalized Gleitkomma können sein zehn- bis hundertmal langsamer als auf normalisiertem Gleitkomma. Dies liegt daran, dass viele Prozessoren sie nicht direkt verarbeiten können und sie mithilfe von Mikrocode auffangen und auflösen müssen.

Wenn Sie die Zahlen nach 10.000 Iterationen ausdrucken, werden Sie feststellen, dass sie abhängig davon, ob sie konvergierten, unterschiedliche Werte haben 0 oder 0.1 wird eingesetzt.

Hier ist der Testcode, der auf x64 kompiliert wurde:

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

Ausgabe:

#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

Beachten Sie, wie im zweiten Lauf die Zahlen sehr nahe bei Null sind.

Denormalisierte Zahlen sind im Allgemeinen selten und daher versuchen die meisten Prozessoren nicht, sie effizient zu handhaben.


Um zu zeigen, dass dies alles mit denormalisierten Zahlen zu tun hat, wenn wir Lege die Denormalen auf Null indem Sie dies zum Anfang des Codes hinzufügen:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Dann die Version mit 0 ist nicht mehr 10x langsamer und wird tatsächlich schneller. (Dazu muss der Code mit aktiviertem SSE kompiliert werden.)

Das bedeutet, dass wir diese seltsamen Werte mit niedrigerer Genauigkeit nicht verwenden, sondern stattdessen nur auf Null runden.

Timings: Core i7 920 bei 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

Am Ende hat das wirklich nichts damit zu tun, ob es eine Ganzzahl oder ein Gleitkomma ist. Das 0 oder 0.1f wird in ein Register außerhalb beider Schleifen umgewandelt / gespeichert. Das hat also keinen Einfluss auf die Performance.


1470
2018-02-16 16:20



Verwenden gcc und das Anwenden eines Diff auf die generierte Assembly ergibt nur diesen Unterschied:

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

Das cvtsi2ssq einer ist in der Tat 10 mal langsamer.

Anscheinend, die float Version verwendet ein XMM Register aus dem Speicher geladen, während die int Version konvertiert ein echtes int Wert 0 bis float Verwendung der cvtsi2ssq Anweisung, viel Zeit nehmend. Vorbeigehen -O3 zu gcc hilft nicht. (gcc Version 4.2.1.)

(Verwendung von double Anstatt von float ist egal, außer dass es die ändert cvtsi2ssq in ein cvtsi2sdq.)

Aktualisieren 

Einige zusätzliche Tests zeigen, dass es nicht unbedingt der ist cvtsi2ssq Anweisung. Einmal eliminiert (mit a int ai=0;float a=ai; und verwenden a Anstatt von 0), bleibt der Geschwindigkeitsunterschied. Also @ Mystical hat recht, die denormalisierten Floats machen den Unterschied. Dies kann durch Testen von Werten zwischen angezeigt werden 0 und 0.1f. Der Wendepunkt im obigen Code ist ungefähr bei 0.00000000000000000000000000000001, wenn die Schleifen plötzlich 10 mal so lang sind.

Aktualisieren Sie << 1 

Eine kleine Visualisierung dieses interessanten Phänomens:

  • Spalte 1: ein Float, geteilt durch 2 für jede Iteration
  • Spalte 2: Die binäre Darstellung dieses Floats
  • Spalte 3: Die Zeit, die benötigt wird, um dieses Float 1e7 mal zu summieren

Sie können deutlich sehen, dass sich der Exponent (die letzten 9 Bits) auf den niedrigsten Wert ändert, wenn die Denormalisierung einsetzt. An diesem Punkt wird die einfache Addition 20 mal langsamer.

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

Eine entsprechende Diskussion über ARM finden Sie in der Frage Stack Overflow Denormalized Fließkomma in Objective-C?.


399
2018-02-16 16:19



Dies liegt an der denormalisierten Fließkomma-Verwendung. Wie man sowohl es als auch die Leistungsstrafe loswird? Nachdem das Internet nach Möglichkeiten gesucht hat, um denormal Zahlen zu töten, scheint es, dass es noch keinen "besten" Weg gibt, dies zu tun. Ich habe diese drei Methoden gefunden, die am besten in verschiedenen Umgebungen funktionieren:

  • Funktioniert möglicherweise nicht in einigen GCC-Umgebungen:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Funktioniert möglicherweise nicht in einigen Visual Studio-Umgebungen: 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)
    
  • Scheint sowohl in GCC als auch in Visual Studio zu funktionieren:

    // 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);
    
  • Der Intel-Compiler verfügt über Optionen, um Denormale standardmäßig auf modernen Intel-CPUs zu deaktivieren. Weitere Details hier

  • Compiler-Schalter. -ffast-math, -msse oder -mfpmath=sse deaktiviert Denormale und macht ein paar andere Dinge schneller, aber leider auch viele andere Annäherungen, die deinen Code kaputt machen könnten. Testen Sie sorgfältig! Das Äquivalent von Fast-Math für den Visual Studio-Compiler ist /fp:fast aber ich konnte nicht bestätigen, ob dadurch auch Denormale ausgeschaltet werden.1


29
2018-02-26 12:15



In gcc können Sie FTZ und DAZ mit diesem aktivieren:

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

Verwende auch gcc switches: -mss -mfpmath = sse

(entsprechende Kredite an Carl Hetherington [1])

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


19
2017-10-02 04:40



Dan Neely's Kommentar sollte zu einer Antwort erweitert werden:

Es ist nicht die Nullkonstante 0.0f Das ist denormalisiert oder verursacht eine Verlangsamung. Es sind die Werte, die bei jeder Iteration der Schleife auf Null gehen. Wenn sie näher und näher an Null kommen, brauchen sie mehr Präzision und sie werden denormalisiert. Dies sind die y[i] Werte. (Sie nähern sich Null, weil x[i]/z[i] ist weniger als 1.0 für alle i.)

Der entscheidende Unterschied zwischen den langsamen und schnellen Versionen des Codes ist die Aussage y[i] = y[i] + 0.1f;. Sobald diese Zeile bei jeder Iteration der Schleife ausgeführt wird, geht die zusätzliche Genauigkeit im Float verloren, und die Denormalisierung, die zur Darstellung dieser Genauigkeit erforderlich ist, wird nicht mehr benötigt. Danach Gleitkommaoperationen an y[i] bleib schnell, weil sie nicht denormalisiert sind.

Warum ist die zusätzliche Genauigkeit verloren, wenn Sie hinzufügen? 0.1f? Weil Fließkommazahlen nur so viele signifikante Ziffern haben. Angenommen, Sie haben genug Speicher für drei signifikante Ziffern 0.00001 = 1e-5, und 0.00001 + 0.1 = 0.1zumindest für dieses Beispiel float-Format, weil es keinen Platz zum Speichern des niedrigstwertigen Bits in hat 0.10001.

Zusamenfassend, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; ist nicht die No-Op, die Sie vielleicht denken, dass es ist.

Mystical sagte das auch: Der Inhalt der Floats ist wichtig, nicht nur der Assembly-Code.


0
2017-08-01 13:32