Warum verlangsamt das Ändern von 0.1f auf 0 die Leistung um das 10-fache?

1527

Warum macht dieses Stück 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; // <--
    }
}

mehr als 10 mal schneller als das folgende Bit laufen (identisch, sofern nicht anders angegeben)?

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. Die Optimierungsstufe war -02mit sse2aktiviert. Ich habe nicht mit anderen Compilern getestet.

Dragarro
quelle
10
Wie haben Sie den Unterschied gemessen? Und welche Optionen haben Sie beim Kompilieren verwendet?
James Kanze
158
Warum lässt der Compiler in diesem Fall nicht einfach die +/- 0 fallen?!?
Michael Dorgan
127
@ Zyx2000 Der Compiler ist nicht annähernd so dumm. Auseinanderbauen ein triviales Beispiel in LINQPad zeigt , dass es den gleichen Code ausspuckt , ob Sie verwenden 0, 0f, 0doder sogar (int)0in einem Kontext , in dem ein doublebenötigt.
Millimoose
14
Was ist die Optimierungsstufe?
Otto Allmendinger

Antworten:

1616

Willkommen in der Welt der denormalisierten Gleitkommazahlen ! Sie können die Leistung zerstören !!!

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

Wenn Sie die Zahlen nach 10.000 Iterationen ausdrucken, werden Sie feststellen, dass sie je nach Verwendung 0oder 0.1Verwendung zu unterschiedlichen Werten konvergiert haben .

Hier ist der auf x64 kompilierte Testcode:

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, dass die Zahlen im zweiten Durchgang sehr nahe bei Null liegen.

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


Um zu demonstrieren, dass dies alles mit denormalisierten Zahlen zu tun hat, wenn wir Denormals auf Null setzen, indem wir dies am Anfang des Codes hinzufügen:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Dann ist die Version mit 0nicht mehr 10x langsamer und wird tatsächlich schneller. (Dies erfordert, dass der Code mit aktiviertem SSE kompiliert wird.)

Dies bedeutet, dass wir anstatt dieser seltsamen Werte mit niedrigerer Genauigkeit von nahezu Null zu verwenden, 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 dies wirklich nichts damit zu tun, ob es sich um eine Ganzzahl oder einen Gleitkomma handelt. Das 0oder 0.1fwird in ein Register außerhalb beider Schleifen konvertiert / gespeichert. Das hat also keinen Einfluss auf die Leistung.

Mystisch
quelle
100
Ich finde es immer noch etwas seltsam, dass die "+ 0" vom Compiler standardmäßig nicht vollständig optimiert wird. Wäre das passiert, wenn er "+ 0.0f" gesetzt hätte?
s73v3r
51
@ s73v3r Das ist eine sehr gute Frage. Jetzt, wo ich mir die Baugruppe anschaue, wird sie nicht einmal + 0.0foptimiert. Wenn ich raten + 0.0fmüsste , könnte es sein, dass es Nebenwirkungen hat, wenn y[i]es sich um ein Signal handelt NaNoder so ... Ich könnte mich jedoch irren.
Mysticial
14
Doppel werden in vielen Fällen immer noch auf dasselbe Problem stoßen, nur mit einer anderen numerischen Größe. Flush-to-Zero ist in Ordnung für Audioanwendungen (und andere, bei denen Sie es sich leisten können, hier und da 1e-38 zu verlieren), aber ich glaube, dass dies nicht für x87 gilt. Ohne FTZ besteht die übliche Lösung für Audioanwendungen darin, ein DC-Signal oder ein Rechtecksignal mit sehr geringer Amplitude (nicht hörbar) in Jitterzahlen zu injizieren, die von der Denormalität entfernt sind.
Russell Borogove
16
@Isaac, denn wenn y [i] deutlich kleiner als 0,1 ist, führt dies zu einem Genauigkeitsverlust, da die höchstwertige Ziffer in der Zahl höher wird.
Dan spielt
167
@ s73v3r: Die + 0.f kann nicht optimiert werden, da der Gleitkomma eine negative 0 hat und das Ergebnis der Addition von + 0.f zu -.0f + 0.f ist. Das Hinzufügen von 0.f ist also keine Identitätsoperation und kann nicht optimiert werden.
Eric Postpischil
415

Das Verwenden gccund 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

Der cvtsi2ssqeine ist in der Tat zehnmal langsamer.

Anscheinend verwendet die floatVersion ein aus dem Speicher geladenes XMM- Register, während die intVersion einen realen intWert 0 in die floatVerwendung des cvtsi2ssqBefehls konvertiert , was viel Zeit in Anspruch nimmt. Die Übergabe -O3an gcc hilft nicht. (gcc Version 4.2.1.)

(Verwenden doublestatt statt floatspielt keine Rolle, außer dass es das cvtsi2ssqin a ändert cvtsi2sdq.)

Aktualisieren

Einige zusätzliche Tests zeigen, dass dies nicht unbedingt die cvtsi2ssqAnweisung ist. Einmal eliminiert (mit a int ai=0;float a=ai;und aanstelle von 0), bleibt der Geschwindigkeitsunterschied bestehen. @Mysticial ist also richtig, die denormalisierten Floats machen den Unterschied. Dies kann durch Testen der Werte zwischen 0und festgestellt werden 0.1f. Der Wendepunkt im obigen Code liegt ungefähr bei 0.00000000000000000000000000000001, wenn die Schleifen plötzlich zehnmal so lange dauern.

Update << 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 diesen Float 1e7-mal zu summieren

Sie können deutlich sehen, dass sich der Exponent (die letzten 9 Bits) auf seinen niedrigsten Wert ändert, wenn die Denormalisierung einsetzt. Zu diesem Zeitpunkt 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 äquivalente Diskussion über ARM finden Sie in der Frage zum Stapelüberlauf Denormalisierter Gleitkomma in Objective-C? .

mvds
quelle
27
-Os nicht reparieren, aber -ffast-mathtut. (Ich benutze das die ganze Zeit, IMO die
Eckfälle
Mit gcc-4.6 findet bei keiner positiven Optimierungsstufe eine Konvertierung statt.
Jed
@leftaroundabout: Kompilieren einer ausführbaren Datei (keine Bibliothek) mit -ffast-mathLinks zu zusätzlichem Startcode, der FTZ (Flush to Zero) und DAZ (Denormal Are Zero) im MXCSR setzt, sodass die CPU für Denormals niemals eine langsame Mikrocode-Unterstützung benötigen muss.
Peter Cordes
34

Dies ist auf die denormalisierte Verwendung von Gleitkommazahlen zurückzuführen. Wie kann man es und die Leistungsstrafe loswerden? Nachdem das Internet nach Möglichkeiten durchsucht wurde, denormale Zahlen zu töten, scheint es noch keinen "besten" Weg zu geben, dies zu tun. Ich habe diese drei Methoden gefunden, die in verschiedenen Umgebungen am besten funktionieren:

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

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Funktioniert in einigen Visual Studio-Umgebungen möglicherweise nicht: 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 bietet Optionen zum Deaktivieren von Denormals auf modernen Intel-CPUs. Weitere Details hier

  • Compiler-Schalter. -ffast-math, -msseOder -mfpmath=ssewird denormals deaktivieren und ein paar andere Dinge schneller machen, aber leider auch tun viele andere Annäherungen, die Ihren Code brechen könnten. Sorgfältig testen! Das Äquivalent von Fast-Math für den Visual Studio-Compiler ist, /fp:fastaber ich konnte nicht bestätigen, ob dies auch Denormals deaktiviert. 1

Feige
quelle
1
Dies klingt nach einer anständigen Antwort auf eine andere, aber verwandte Frage (Wie kann ich verhindern, dass numerische Berechnungen zu denormalen Ergebnissen führen?) Diese Frage wird jedoch nicht beantwortet.
Ben Voigt
Windows X64 übergibt beim Starten von .exe die Einstellung "Abrupter Unterlauf", Windows 32-Bit und Linux nicht. Unter Linux sollte gcc -ffast-math einen plötzlichen Unterlauf einstellen (aber ich denke nicht unter Windows). Intel-Compiler sollen in main () initialisiert werden, damit diese Betriebssystemunterschiede nicht durchgehen, aber ich bin gebissen worden und muss sie explizit im Programm festlegen. Intel-CPUs, die mit Sandy Bridge beginnen, sollen Subnormen, die beim Addieren / Subtrahieren (aber nicht beim Teilen / Multiplizieren) entstehen, effizient verarbeiten. Daher gibt es einen Grund für die Verwendung eines allmählichen Unterlaufs.
Tim18
1
Microsoft / fp: fast (kein Standard) führt keine der aggressiven Dinge aus, die gcc -ffast-math oder ICL (Standard) / fp: fast innewohnen. Es ist eher wie ICL / fp: source. Sie müssen also / fp: (und in einigen Fällen den Unterlaufmodus) explizit festlegen, wenn Sie diese Compiler vergleichen möchten.
Tim18
18

In gcc können Sie FTZ und DAZ folgendermaßen 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);
}

Verwenden Sie auch gcc-Schalter: -msse -mfpmath = sse

(entsprechende Credits an Carl Hetherington [1])

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

Deutscher Garcia
quelle
Siehe auch fesetround()ab fenv.h(definiert für C99) für eine andere, tragbarere Art der Rundung ( linux.die.net/man/3/fesetround ) (dies würde jedoch alle FP-Operationen betreffen, nicht nur Subnormale )
German Garcia
Sind Sie sicher, dass Sie 1 << 15 und 1 << 11 für FTZ benötigen? Ich habe nur 1 << 15 gesehen, das an anderer Stelle zitiert wurde ...
Abb.
@fig: 1 << 11 ist für die Unterlaufmaske. Weitere Infos hier: softpixel.com/~cwright/programming/simd/sse.php
German Garcia
@GermanGarcia Dies beantwortet nicht die Frage des OP. Die Frage lautete: "Warum wird dieses Codebit zehnmal schneller ausgeführt als ...". Sie sollten entweder versuchen, dies zu beantworten, bevor Sie diese Problemumgehung bereitstellen, oder dies in einem Kommentar angeben.
9

Dan Neelys Kommentar sollte zu einer Antwort erweitert werden:

Es ist nicht die Nullkonstante 0.0f, die denormalisiert wird oder eine Verlangsamung verursacht, sondern die Werte, die sich bei jeder Iteration der Schleife Null nähern. Je näher sie Null kommen, desto präziser müssen sie dargestellt werden, und sie werden denormalisiert. Das sind die y[i]Werte. (Sie nähern sich Null, weil sie x[i]/z[i]für alle weniger als 1,0 sind i.)

Der entscheidende Unterschied zwischen der langsamen und der schnellen Version 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 zur Darstellung dieser Genauigkeit erforderliche Denormalisierung wird nicht mehr benötigt. Danach y[i]bleiben Gleitkommaoperationen an schnell, da sie nicht denormalisiert werden.

Warum geht die zusätzliche Präzision verloren, wenn Sie hinzufügen 0.1f? Weil Gleitkommazahlen nur so viele signifikante Stellen haben. Angenommen, Sie haben genug Speicherplatz für drei signifikante Stellen 0.00001 = 1e-5und 0.00001 + 0.1 = 0.1zumindest für dieses Beispiel das Float-Format, da dort kein Platz für das niedrigstwertige Bit vorhanden ist 0.10001.

Kurz gesagt, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;ist das nicht das No-Op, von dem Sie vielleicht denken, dass es es ist.

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

Remcycles
quelle