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




flush to zero (4)

Willkommen in der Welt des denormalisierten Gleitkommas ! 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 demormalisierten Gleitkomma können zehn- bis hundertmal langsamer als normalisierte Gleitkommazahlen sein. 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 zu unterschiedlichen Werten konvergiert sind, je nachdem, ob 0 oder 0.1 verwendet wird.

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 demonstrieren, dass dies alles mit denormalisierten Zahlen zu tun hat, wenn wir Denormale auf Null setzen, indem wir das zum Anfang des Codes hinzufügen:

_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 sein.)

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. Die 0 oder 0.1f wird in ein Register außerhalb beider Schleifen umgewandelt / gespeichert. Das hat also keinen Einfluss auf die Performance.

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.)


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 machen Sie auch viele andere Näherungen, die Ihren Code -mfpmath=sse 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 deaktiviert werden. 1


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] 1


Wenn Sie gcc und ein Diff auf die generierte Assembly anwenden, ergibt sich nur dieser Unterschied:

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

Der cvtsi2ssq ist in der Tat 10 mal langsamer.

Scheinbar verwendet die float Version ein XMM Register, das aus dem Speicher geladen wird, während die int Version einen reellen int Wert 0 unter Verwendung der cvtsi2ssq Anweisung in float cvtsi2ssq , was viel Zeit in cvtsi2ssq nimmt. Die -O3 von -O3 an gcc hilft nicht. (gcc Version 4.2.1.)

(Die Verwendung von double anstelle von float spielt keine Rolle, außer dass es cvtsi2ssq in cvtsi2sdq .)

Aktualisieren

Einige zusätzliche Tests zeigen, dass es nicht unbedingt die cvtsi2ssq Anweisung ist. Einmal beseitigt (mit einem int ai=0;float a=ai; und mit a statt 0 ), bleibt die Geschwindigkeitsdifferenz bestehen. Also @ Mystical hat recht, die denormalisierten Floats machen den Unterschied. Dies kann durch Testen von Werten zwischen 0 und 0.1f . Der Wendepunkt im obigen Code liegt 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 Denormalized Floating Point in Objective-C? .





floating-point