c++ - mantissa - flush to zero




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

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

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


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


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.





floating-point