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

Lesezeit: 6 Minuten

Warum verlangsamt das Andern von 01f auf 0 die Leistung
Glasfische

Warum wird 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 laufen als das folgende Bit (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 -02 mit sse2 aktiviert. Ich habe nicht mit anderen Compilern getestet.

  • Wie hast du den Unterschied gemessen? Und welche Optionen hast du beim Kompilieren verwendet?

    – James Kanze

    16. Februar 2012 um 16:19 Uhr

  • Warum lässt der Compiler in diesem Fall nicht einfach +/- 0 fallen?!?

    – Michael Dorgan

    16. Februar 2012 um 16:25 Uhr

  • @ Zyx2000 Der Compiler ist nicht annähernd so dumm. Das Zerlegen eines trivialen Beispiels in LINQPad zeigt, dass es denselben Code ausspuckt, egal ob Sie es verwenden 0, 0f, 0doder auch (int)0 in einem Kontext, in dem a double wird gebraucht.

    – Millielch

    17. Februar 2012 um 2:20 Uhr

  • Was ist die Optimierungsstufe?

    – Otto Allmendinger

    17. Februar 2012 um 8:02 Uhr

  • Warum löscht der Compiler eigentlich nicht +/-0?

    – Vorac

    10. Mai 2013 um 7:12 Uhr

Warum verlangsamt das Andern von 01f auf 0 die Leistung
Mystisch

Willkommen in der Welt von denormalisiertes Fließkomma! Sie können die Leistung verheeren!!!

Denormale (oder subnormale) Zahlen sind eine Art Hack, um einige zusätzliche Werte aus der Fließkommadarstellung sehr nahe an Null zu bringen. Operationen mit denormalisierten Gleitkommazahlen können sein zehn- bis hundertmal langsamer als bei normalisiertem Fließkomma. Dies liegt daran, dass viele Prozessoren sie nicht direkt verarbeiten können und sie unter Verwendung von Mikrocode abfangen und auflösen müssen.

Wenn Sie die Zahlen nach 10.000 Iterationen ausdrucken, werden Sie sehen, dass sie je nach ob zu unterschiedlichen Werten konvergiert sind 0 oder 0.1 wird genutzt.

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 Lauf sehr nahe bei Null liegen.

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 Flush Denormals auf Null indem Sie dies am Anfang des Codes hinzufügen:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

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

Das bedeutet, dass wir, anstatt diese seltsamen Werte mit niedrigerer Genauigkeit von fast Null zu verwenden, stattdessen einfach auf Null runden.

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

Letztendlich hat das wirklich nichts damit zu tun, ob es sich um eine Ganzzahl oder eine Fließkommazahl handelt. Die 0 oder 0.1f außerhalb beider Schleifen in ein Register umgewandelt/gespeichert. Auf die Leistung hat das also keinen Einfluss.

  • Ich finde es immer noch etwas seltsam, dass das “+ 0” standardmäßig nicht vollständig vom Compiler optimiert wird. Wäre das passiert, wenn er “+ 0.0f” gesetzt hätte?

    – s73v3r

    16. Februar 2012 um 19:10 Uhr

  • @s73v3r Das ist eine sehr gute Frage. Jetzt, wo ich mir die Montage anschaue, nicht einmal + 0.0f wird optimiert. Wenn ich raten müsste, könnte es das sein + 0.0f hätte Nebenwirkungen, wenn y[i] war zufällig eine Signalisierung NaN oder so … Ich könnte mich aber irren.

    – Mystisch

    16. Februar 2012 um 19:31 Uhr

  • Doubles werden in vielen Fällen immer noch auf das gleiche Problem stoßen, nur bei einer anderen numerischen Größenordnung. 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, es gilt nicht für x87. Ohne FTZ besteht die übliche Lösung für Audioanwendungen darin, ein DC- oder Rechteckwellensignal mit sehr niedriger Amplitude (nicht hörbar) einzuspeisen, um die Jitterzahlen von der Denormalität zu entfernen.

    – Russell Borogove

    17. Februar 2012 um 0:12 Uhr

  • @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 bei Feuerschein

    17. Februar 2012 um 13:28 Uhr

  • @ s73v3r: Das +0.f kann nicht optimiert werden, da 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

    6. Juli 2012 um 17:59 Uhr

1647127815 120 Warum verlangsamt das Andern von 01f auf 0 die Leistung
mvds

Verwenden gcc und das Anwenden eines Unterschieds auf die generierte Baugruppe ergibt nur diesen Unterschied:

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

Die cvtsi2ssq einer ist in der Tat 10 mal langsamer.

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

(Verwenden double anstatt float egal, außer dass es das ändert cvtsi2ssq in ein cvtsi2sdq.)

Aktualisieren

Einige Extratests zeigen, dass es das nicht unbedingt ist cvtsi2ssq Anweisung. Einmal eliminiert (mit a int ai=0;float a=ai; und verwenden a anstatt 0), bleibt die Geschwindigkeitsdifferenz bestehen. @Mystcial hat also Recht, die denormalisierten Floats machen den Unterschied. Dies kann durch Testen von Werten zwischen gesehen werden 0 und 0.1f. Der Wendepunkt im obigen Code liegt ungefähr bei 0.00000000000000000000000000000001wenn die Schleifen plötzlich 10 mal so lange dauern.

Aktualisieren<<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, wie sich der Exponent (die letzten 9 Bits) auf seinen 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 Stack Overflow-Frage Denormalisiertes Fließkomma in Objective-C?.

  • -Os nicht beheben, aber -ffast-math tut. (Ich benutze das die ganze Zeit, IMO sollten die Eckfälle, in denen es Präzisionsprobleme verursacht, sowieso nicht in einem richtig entworfenen Programm auftauchen.)

    – linksherum

    17. Februar 2012 um 10:17 Uhr

  • Bei gcc-4.6 gibt es auf keiner positiven Optimierungsstufe eine Konvertierung.

    – Jed

    11. März 2012 um 16:14 Uhr

  • @leftaroundabout: Kompilieren einer ausführbaren Datei (nicht Bibliothek) mit -ffast-math verknüpft einen zusätzlichen Startcode, der FTZ (flush to zero) und DAZ (denormal are zero) im MXCSR setzt, sodass die CPU niemals eine langsame Microcode-Unterstützung für Denormals nehmen muss.

    – Peter Cordes

    16. Januar 2019 um 10:23 Uhr

1647127816 148 Warum verlangsamt das Andern von 01f auf 0 die Leistung
Feige

Dies liegt an der Verwendung von denormalisierten Gleitkommazahlen. Wie kann man es und die Leistungseinbuße loswerden? Nachdem ich das Internet nach Wegen durchforstet habe, um 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 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 zum standardmäßigen Deaktivieren von Denormals auf modernen Intel-CPUs. Weitere Details hier

  • Compiler-Schalter. -ffast-math, -msse oder -mfpmath=sse wird Denormals deaktivieren und ein paar andere Dinge schneller machen, aber leider auch viele andere Annäherungen machen, die Ihren Code beschädigen 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 dies auch Denormals deaktiviert.1

  • Dies klingt nach einer anständigen Antwort auf eine andere, aber verwandte Frage (Wie kann ich verhindern, dass numerische Berechnungen denormale Ergebnisse liefern?). Diese Frage wird jedoch nicht beantwortet.

    – Ben Voigt

    21. Juni 2014 um 21:28 Uhr

  • Windows X64 übergibt eine Einstellung für abrupten Unterlauf, wenn es .exe startet, während Windows 32-Bit und Linux dies nicht tun. Unter Linux sollte gcc -ffast-math einen abrupten Unterlauf setzen (aber ich denke nicht unter Windows). Intel-Compiler sollen in main() initialisieren, damit diese Betriebssystemunterschiede nicht durchgehen, aber ich wurde gebissen und muss es explizit im Programm festlegen. Intel-CPUs, die mit Sandy Bridge beginnen, sollen Subnormale, die beim Addieren/Subtrahieren (aber nicht beim Dividieren/Multiplizieren) auftreten, effizient verarbeiten, daher gibt es einen Grund für die Verwendung eines allmählichen Unterlaufs.

    – Tim18

    11. Juni 2016 um 20:03 Uhr

  • Microsoft /fp:fast (kein Standard) macht keine der aggressiven Dinge, 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

    11. Juni 2016 um 20:09 Uhr

In gcc können Sie FTZ und DAZ damit 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

1647127817 620 Warum verlangsamt das Andern von 01f auf 0 die Leistung
recycelt

Dan Neelys Kommentar sollte zu einer Antwort erweitert werden:

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

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

Warum geht die zusätzliche Genauigkeit verloren, wenn Sie hinzufügen 0.1f? Weil Gleitkommazahlen nur so viele signifikante Stellen haben. Angenommen, Sie haben dann genug Speicherplatz für drei signifikante Ziffern 0.00001 = 1e-5und 0.00001 + 0.1 = 0.1zumindest für dieses Float-Beispielformat, da es keinen Platz zum Speichern des niedrigstwertigen Bits bietet 0.10001.

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

Mystical hat das auch gesagt: Der Inhalt der Schwimmer ist wichtig, nicht nur der Assemblercode.

BEARBEITEN: Um dies genauer zu erläutern, benötigt nicht jede Gleitkommaoperation die gleiche Zeit, um ausgeführt zu werden, selbst wenn der Maschinen-Opcode derselbe ist. Bei einigen Operanden/Eingängen dauert die Ausführung derselben Anweisung länger. Dies gilt insbesondere für denormale Zahlen.

995390cookie-checkWarum verlangsamt das Ändern von 0,1f auf 0 die Leistung um das 10-fache?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy