
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.

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.

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.00000000000000000000000000000001
wenn 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?.

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

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-5
und 0.00001 + 0.1 = 0.1
zumindest 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.
9953900cookie-checkWarum verlangsamt das Ändern von 0,1f auf 0 die Leistung um das 10-fache?yes
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
,0d
oder auch(int)0
in einem Kontext, in dem adouble
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