it-swarm.com.de

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

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

laufen Sie mehr als 10 Mal schneller 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. (Ich habe nicht mit anderen Compilern getestet.)

1468
Dragarro

Willkommen in der Welt von denormalisierten Gleitkommazahlen ! Sie können die Leistung ruinieren !!!

Denormale (oder subnormale) Zahlen sind eine Art Hack, um einige zusätzliche Werte aus der Gleitkommadarstellung zu erhalten, die sehr nahe an Null liegen. Operationen mit denormalisierten Gleitkommazahlen können zehn- bis hundertmal langsamer sein als mit normalisierten Gleitkommazahlen . 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 je nachdem, ob 0 oder 0.1 verwendet wird, zu unterschiedlichen Werten konvergiert sind.

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.

Denormalisierte 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 die Denormale auf Null setzen , indem wir dies an den Anfang des Codes setzen:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

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

Dies bedeutet, dass wir statt dieser seltsamen Werte mit einer niedrigeren Genauigkeit von nahezu Null lediglich 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

Letztendlich hat dies wirklich nichts damit zu tun, ob es sich um eine Ganzzahl oder einen Gleitkommawert handelt. Der 0 oder 0.1f wird in ein Register außerhalb beider Schleifen konvertiert/gespeichert. Das hat also keine Auswirkung auf die Leistung.

1561
Mysticial

Die Verwendung von gcc und das Anwenden eines Diffs 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 cvtsi2ssq ist in der Tat zehnmal langsamer.

Anscheinend verwendet die float -Version ein XMM -Register, das aus dem Speicher geladen wurde, während die int -Version einen echten int -Wert 0 in float konvertiert. Verwenden Sie die Anweisung cvtsi2ssq, und nehmen Sie sich viel Zeit. Es hilft nicht, -O3 an gcc zu übergeben. (gcc version 4.2.1.)

(Die Verwendung von double anstelle von float spielt keine Rolle, außer dass der cvtsi2ssq in einen cvtsi2sdq geändert wird.)

Update

Einige zusätzliche Tests zeigen, dass es sich nicht unbedingt um die Anweisung cvtsi2ssq handelt. Einmal eliminiert (mit einem int ai=0;float a=ai; und mit a anstelle von 0), bleibt der Geschwindigkeitsunterschied bestehen. @Mysticial hat also recht, die denormalisierten Floats machen den Unterschied. Dies kann durch Testen von Werten zwischen 0 und 0.1f festgestellt werden. Der Wendepunkt im obigen Code liegt ungefähr bei 0.00000000000000000000000000000001, wenn die Schleifen plötzlich zehnmal länger 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 der Exponent (die letzten 9 Bits) sich zu seinem 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 zu ARM finden Sie in Stack Overflow question Denormalized Floating Point in Objective-C?.

408
mvds

Dies ist auf die Verwendung von denormalisierten Gleitkommazahlen zurückzuführen. Wie man es und die Leistungsstrafe loswird? Nachdem das Internet nach Wegen durchsucht wurde, um normale 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 können:

  • Funktioniert möglicherweise nicht in allen 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 Deaktivieren von Denormalen auf modernen Intel-CPUs. Mehr Details hier

  • Compiler wechselt. -ffast-math, -msse oder -mfpmath=sse deaktivieren denormals und beschleunigen einige andere Dinge, aber leider auch viele andere Annäherungen, die Ihren Code beschädigen könnten. Sorgfältig testen! Das Äquivalent zu fast-math für den Visual Studio-Compiler ist /fp:fast, aber ich konnte nicht bestätigen, ob dadurch auch denormals deaktiviert werden. . 1

33
fig

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

(entsprechend an Carl Hetherington [1])

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

19
German Garcia

Dan Neelys Kommentar sollte zu einer Antwort erweitert werden:

Es ist nicht die Null-Konstante 0.0f, die denormalisiert ist oder eine Verlangsamung verursacht, es sind die Werte, die sich bei jeder Iteration der Schleife Null nähern. Wenn sie näher und näher an Null kommen, müssen sie präziser dargestellt und denormalisiert werden. Dies sind die Werte für y[i]. (Sie nähern sich Null, weil x[i]/z[i] für alle i kleiner als 1.0 ist.)

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 Präzision im Gleitkomma verloren, und die zur Darstellung dieser Präzision erforderliche Denormalisierung ist nicht mehr erforderlich. Danach bleiben Gleitkommaoperationen für y[i] schnell, da sie nicht denormalisiert sind.

Warum geht die zusätzliche Präzision verloren, wenn Sie 0.1f hinzufügen? Weil Fließkommazahlen nur so viele signifikante Stellen haben. Angenommen, Sie haben genügend Speicherplatz für drei signifikante Stellen, dann 0.00001 = 1e-5 und 0.00001 + 0.1 = 0.1, zumindest für dieses Beispiel-Float-Format, da in 0.10001 nicht genügend Speicherplatz für das am wenigsten signifikante Bit vorhanden ist.

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

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

4
remicles2