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; // <--
}
}
mehr als 10 mal schneller als das folgende Bit laufen (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.
0
,0f
,0d
oder sogar(int)0
in einem Kontext , in dem eindouble
benötigt.Antworten:
Willkommen in der Welt der denormalisierten Gleitkommazahlen ! Sie können die Leistung zerstören !!!
Denormale (oder subnormale) Zahlen sind eine Art Hack, um einige zusätzliche Werte aus der Gleitkommadarstellung sehr nahe Null zu erhalten. Operationen mit denormalisiertem Gleitkomma können zehn- bis hundertmal langsamer sein als mit normalisiertem Gleitkomma. Dies liegt daran, dass viele Prozessoren sie nicht direkt verarbeiten können und sie mithilfe von Mikrocode abfangen und auflösen müssen.
Wenn Sie die Zahlen nach 10.000 Iterationen ausdrucken, werden Sie feststellen, dass sie je nach Verwendung
0
oder0.1
Verwendung zu unterschiedlichen Werten konvergiert haben .Hier ist der auf x64 kompilierte Testcode:
Ausgabe:
Beachten Sie, dass die Zahlen im zweiten Durchgang sehr nahe bei Null liegen.
Denormierte 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 Denormals auf Null setzen, indem wir dies am Anfang des Codes hinzufügen:
Dann ist die Version mit
0
nicht mehr 10x langsamer und wird tatsächlich schneller. (Dies erfordert, dass der Code mit aktiviertem SSE kompiliert wird.)Dies bedeutet, dass wir anstatt dieser seltsamen Werte mit niedrigerer Genauigkeit von nahezu Null zu verwenden, stattdessen nur auf Null runden.
Timings: Core i7 920 bei 3,5 GHz:
Am Ende hat dies wirklich nichts damit zu tun, ob es sich um eine Ganzzahl oder einen Gleitkomma handelt. Das
0
oder0.1f
wird in ein Register außerhalb beider Schleifen konvertiert / gespeichert. Das hat also keinen Einfluss auf die Leistung.quelle
+ 0.0f
optimiert. Wenn ich raten+ 0.0f
müsste , könnte es sein, dass es Nebenwirkungen hat, wenny[i]
es sich um ein Signal handeltNaN
oder so ... Ich könnte mich jedoch irren.Das Verwenden
gcc
und Anwenden eines Diff auf die generierte Assembly ergibt nur diesen Unterschied:Der
cvtsi2ssq
eine ist in der Tat zehnmal langsamer.Anscheinend verwendet die
float
Version ein aus dem Speicher geladenes XMM- Register, während dieint
Version einen realenint
Wert 0 in diefloat
Verwendung descvtsi2ssq
Befehls konvertiert , was viel Zeit in Anspruch nimmt. Die Übergabe-O3
an gcc hilft nicht. (gcc Version 4.2.1.)(Verwenden
double
statt stattfloat
spielt keine Rolle, außer dass es dascvtsi2ssq
in a ändertcvtsi2sdq
.)Aktualisieren
Einige zusätzliche Tests zeigen, dass dies nicht unbedingt die
cvtsi2ssq
Anweisung ist. Einmal eliminiert (mit aint ai=0;float a=ai;
unda
anstelle von0
), bleibt der Geschwindigkeitsunterschied bestehen. @Mysticial ist also richtig, die denormalisierten Floats machen den Unterschied. Dies kann durch Testen der Werte zwischen0
und festgestellt werden0.1f
. Der Wendepunkt im obigen Code liegt ungefähr bei0.00000000000000000000000000000001
, wenn die Schleifen plötzlich zehnmal so lange dauern.Update << 1
Eine kleine Visualisierung dieses interessanten Phänomens:
Sie können deutlich sehen, dass sich der Exponent (die letzten 9 Bits) auf seinen niedrigsten Wert ändert, wenn die Denormalisierung einsetzt. Zu diesem Zeitpunkt wird die einfache Addition 20-mal langsamer.
Eine äquivalente Diskussion über ARM finden Sie in der Frage zum Stapelüberlauf Denormalisierter Gleitkomma in Objective-C? .
quelle
-O
s nicht reparieren, aber-ffast-math
tut. (Ich benutze das die ganze Zeit, IMO die-ffast-math
Links zu zusätzlichem Startcode, der FTZ (Flush to Zero) und DAZ (Denormal Are Zero) im MXCSR setzt, sodass die CPU für Denormals niemals eine langsame Mikrocode-Unterstützung benötigen muss.Dies ist auf die denormalisierte Verwendung von Gleitkommazahlen zurückzuführen. Wie kann man es und die Leistungsstrafe loswerden? Nachdem das Internet nach Möglichkeiten durchsucht wurde, 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 in einigen GCC-Umgebungen möglicherweise nicht:
Funktioniert in einigen Visual Studio-Umgebungen möglicherweise nicht: 1
Scheint sowohl in GCC als auch in Visual Studio zu funktionieren:
Der Intel-Compiler bietet Optionen zum 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 tun viele andere Annäherungen, die Ihren Code brechen könnten. Sorgfältig testen! 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. 1quelle
In gcc können Sie FTZ und DAZ folgendermaßen aktivieren:
Verwenden Sie auch gcc-Schalter: -msse -mfpmath = sse
(entsprechende Credits an Carl Hetherington [1])
[1] http://carlh.net/plugins/denormals.php
quelle
fesetround()
abfenv.h
(definiert für C99) für eine andere, tragbarere Art der Rundung ( linux.die.net/man/3/fesetround ) (dies würde jedoch alle FP-Operationen betreffen, nicht nur Subnormale )Dan Neelys Kommentar sollte zu einer Antwort erweitert werden:
Es ist nicht die Nullkonstante
0.0f
, die denormalisiert wird oder eine Verlangsamung verursacht, sondern die Werte, die sich bei jeder Iteration der Schleife Null nähern. Je näher sie Null kommen, desto präziser müssen sie dargestellt werden, und sie werden denormalisiert. Das sind diey[i]
Werte. (Sie nähern sich Null, weil siex[i]/z[i]
für alle weniger als 1,0 sindi
.)Der entscheidende Unterschied zwischen der langsamen und der schnellen Version des Codes ist die Aussage
y[i] = y[i] + 0.1f;
. Sobald diese Zeile bei jeder Iteration der Schleife ausgeführt wird, geht die zusätzliche Genauigkeit im Float verloren, und die zur Darstellung dieser Genauigkeit erforderliche Denormalisierung wird nicht mehr benötigt. Danachy[i]
bleiben Gleitkommaoperationen an schnell, da sie nicht denormalisiert werden.Warum geht die zusätzliche Präzision verloren, wenn Sie hinzufügen
0.1f
? Weil Gleitkommazahlen nur so viele signifikante Stellen haben. Angenommen, Sie haben genug Speicherplatz für drei signifikante Stellen0.00001 = 1e-5
und0.00001 + 0.1 = 0.1
zumindest für dieses Beispiel das Float-Format, da dort kein Platz für das niedrigstwertige Bit vorhanden ist0.10001
.Kurz gesagt,
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
ist das nicht das No-Op, von dem Sie vielleicht denken, dass es es ist.Mystical sagte dies auch : Der Inhalt der Floats ist wichtig, nicht nur der Assembler-Code.
quelle