Ich mache einige leistungskritische Arbeiten in C ++ und wir verwenden derzeit ganzzahlige Berechnungen für Probleme, die von Natur aus Gleitkommawerte sind, weil "es schneller ist". Dies verursacht eine Menge nerviger Probleme und fügt viel nervigen Code hinzu.
Jetzt erinnere ich mich, dass ich gelesen habe, wie langsam Gleitkommaberechnungen ungefähr in den 386 Tagen waren, in denen es meines Erachtens (IIRC) einen optionalen Co-Prozessor gab. Aber heutzutage macht es bei exponentiell komplexeren und leistungsfähigeren CPUs sicherlich keinen Unterschied in der "Geschwindigkeit", wenn Gleitkomma- oder Ganzzahlberechnungen durchgeführt werden? Zumal die tatsächliche Berechnungszeit im Vergleich zu einem Pipeline-Stillstand oder dem Abrufen von Daten aus dem Hauptspeicher winzig ist?
Ich weiß, dass die richtige Antwort darin besteht, die Zielhardware zu bewerten. Was wäre ein guter Weg, um dies zu testen? Ich habe zwei winzige C ++ - Programme geschrieben und ihre Laufzeit mit "time" unter Linux verglichen, aber die tatsächliche Laufzeit ist zu variabel (hilft nicht, dass ich auf einem virtuellen Server ausgeführt werde). Kann ich nicht meinen ganzen Tag damit verbringen, Hunderte von Benchmarks durchzuführen, Diagramme zu erstellen usw. Kann ich etwas tun, um einen vernünftigen Test der relativen Geschwindigkeit zu erhalten? Irgendwelche Ideen oder Gedanken? Bin ich völlig falsch
Die Programme, die ich wie folgt verwendet habe, sind keineswegs identisch:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
int accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += rand( ) % 365;
}
std::cout << accum << std::endl;
return 0;
}
Programm 2:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
float accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += (float)( rand( ) % 365 );
}
std::cout << accum << std::endl;
return 0;
}
Danke im Voraus!
Bearbeiten: Die Plattform, die mir wichtig ist, ist reguläres x86 oder x86-64, das auf Desktop-Linux- und Windows-Computern ausgeführt wird.
Bearbeiten 2 (eingefügt aus einem Kommentar unten): Wir haben derzeit eine umfangreiche Codebasis. Ich bin wirklich auf die Verallgemeinerung gestoßen, dass wir "kein Float verwenden dürfen, da die Ganzzahlberechnung schneller ist" - und ich suche nach einer Möglichkeit (wenn dies überhaupt zutrifft), diese verallgemeinerte Annahme zu widerlegen. Mir ist klar, dass es unmöglich sein würde, das genaue Ergebnis für uns vorherzusagen, wenn wir nicht die ganze Arbeit erledigen und sie anschließend profilieren würden.
Trotzdem vielen Dank für all Ihre hervorragenden Antworten und Ihre Hilfe. Fühlen Sie sich frei, etwas anderes hinzuzufügen :).
quelle
addl
ersetzt durchfadd
zum Beispiel). Der einzige Weg, um wirklich eine gute Messung zu erhalten, besteht darin, einen Kernbestandteil Ihres realen Programms zu erhalten und verschiedene Versionen davon zu profilieren. Leider kann das ohne großen Aufwand ziemlich schwierig sein. Wenn Sie uns die Zielhardware und Ihren Compilerfixed_point
mitteilen, können die Benutzer Ihnen möglicherweise zumindest bereits vorhandene Erfahrungen usw. vermitteln. Ich vermute, Sie könnten eine Art Vorlagenklasse erstellen, die diese Arbeit erheblich erleichtert.float
den Geschwindigkeitsschub erhält, dies aber normalerweisedouble
nicht tut.Antworten:
Leider kann ich Ihnen nur eine "es kommt darauf an" Antwort geben ...
Nach meiner Erfahrung gibt es viele, viele Variablen für die Leistung ... insbesondere zwischen Ganzzahl- und Gleitkomma-Mathematik. Es variiert stark von Prozessor zu Prozessor (sogar innerhalb derselben Familie wie x86), da verschiedene Prozessoren unterschiedliche "Pipeline" -Längen haben. Außerdem sind einige Vorgänge im Allgemeinen sehr einfach (z. B. Addition) und haben einen beschleunigten Weg durch den Prozessor, während andere (z. B. Division) viel, viel länger dauern.
Die andere große Variable ist, wo sich die Daten befinden. Wenn Sie nur wenige Werte hinzufügen müssen, können sich alle Daten im Cache befinden, wo sie schnell an die CPU gesendet werden können. Eine sehr, sehr langsame Gleitkommaoperation, bei der sich die Daten bereits im Cache befinden, ist um ein Vielfaches schneller als eine Ganzzahloperation, bei der eine Ganzzahl aus dem Systemspeicher kopiert werden muss.
Ich gehe davon aus, dass Sie diese Frage stellen, weil Sie an einer leistungskritischen Anwendung arbeiten. Wenn Sie für die x86-Architektur entwickeln und zusätzliche Leistung benötigen, sollten Sie die Verwendung der SSE-Erweiterungen in Betracht ziehen. Dies kann die Gleitkomma-Arithmetik mit einfacher Genauigkeit erheblich beschleunigen, da dieselbe Operation für mehrere Daten gleichzeitig ausgeführt werden kann und es eine separate * Bank von Registern für die SSE-Operationen gibt. (Ich habe in Ihrem zweiten Beispiel festgestellt, dass Sie "float" anstelle von "double" verwendet haben, was mich glauben lässt, dass Sie Mathematik mit einfacher Genauigkeit verwenden.)
* Hinweis: Die Verwendung der alten MMX-Anweisungen würde Programme tatsächlich verlangsamen, da diese alten Anweisungen tatsächlich dieselben Register wie die FPU verwendeten, so dass es unmöglich ist, sowohl die FPU als auch die MMX gleichzeitig zu verwenden.
quelle
double
-präzisions-FP gepackt . Mit nur zwei 64-Bit-double
s pro Register ist die potenzielle Beschleunigung geringer alsfloat
bei Code, der gut vektorisiert. Skalierenfloat
unddouble
verwenden Sie XMM-Register auf x86-64, wobei nur x87 verwendet wirdlong double
. (Also @ Dan: Nein, MMX-Register stehen nicht in Konflikt mit normalen FPU-Registern, da normale FPU auf x86-64 die SSE-Einheit ist. MMX wäre sinnlos, denn wenn Sie ganzzahliges SIMD ausführen können, möchten Sie 16-Bytexmm0..15
anstelle von 8 -Bytemm0..7
und moderne CPUs haben einen schlechteren MMX als SSE-Durchsatz.)Zum Beispiel (kleinere Zahlen sind schneller),
64-Bit Intel Xeon X5550 bei 2,67 GHz, gcc 4.1.2
-O3
32-Bit-Dual-Core-AMD-Opteron (tm) -Prozessor 265 bei 1,81 GHz, gcc 3.4.6
-O3
Wie Dan wies darauf hin , auch wenn Sie normalisieren für Taktfrequenz (die in Pipeline- Designs irreführend selbst werden kann), wird variieren die Ergebnisse auf CPU - Architektur wild Basis (einzelne ALU / FPU Leistung , sowie tatsächliche Anzahl der ALUs / FPU verfügbar pro Kern in superskalaren Designs, der beeinflusst, wie viele unabhängige Operationen parallel ausgeführt werden können - letzterer Faktor wird vom folgenden Code nicht ausgeübt, da alle folgenden Operationen nacheinander abhängig sind.)
FPU / ALU-Betriebsbenchmark des armen Mannes:
quelle
volatile
, um sicherzugehen. Unter Win64 wird die FPU nicht verwendet und MSVC generiert keinen Code dafür. Daher werden dortmulss
unddivss
XMM-Anweisungen kompiliert, die 25-mal schneller sind als die FPU in Win32. Testmaschine ist Core i5 M 520 bei 2,40 GHzv
erreichen sehr schnell entweder 0 oder +/- inf, was von bestimmten fpu-Implementierungen (theoretisch) als Sonderfall / Fastpatheed behandelt werden kann oder nicht.v
). Bei neueren Intel-Designs ist Divide überhaupt nicht per Pipeline (divss
/divps
hat eine Latenz von 10 bis 14 Zyklen und den gleichen gegenseitigen Durchsatz).mulss
Die Latenz beträgt jedoch 5 Zyklen, kann jedoch bei jedem Zyklus eine Latenz ausgeben. (Oder zwei pro Zyklus auf Haswell, da Port 0 und Port 1 beide einen Multiplikator für FMA haben).Es gibt wahrscheinlich einen signifikanten Unterschied in der realen Geschwindigkeit zwischen Festkomma- und Gleitkomma-Mathematik, aber der theoretische Best-Case-Durchsatz der ALU gegenüber der FPU ist völlig irrelevant. Stattdessen die Anzahl der Ganzzahl- und Gleitkommaregister (reelle Register, keine Registernamen) in Ihrer Architektur, die von Ihrer Berechnung nicht anderweitig verwendet werden (z. B. zur Schleifensteuerung), die Anzahl der Elemente jedes Typs, die in eine Cache-Zeile passen Optimierungen möglich unter Berücksichtigung der unterschiedlichen Semantik für Ganzzahl- und Gleitkomma-Mathematik - diese Effekte werden dominieren. Die Datenabhängigkeiten Ihres Algorithmus spielen hier eine wichtige Rolle, sodass kein allgemeiner Vergleich die Leistungslücke für Ihr Problem vorhersagen kann.
Die Ganzzahladdition ist beispielsweise kommutativ. Wenn der Compiler also eine Schleife sieht, wie Sie sie für einen Benchmark verwendet haben (vorausgesetzt, die Zufallsdaten wurden im Voraus vorbereitet, damit die Ergebnisse nicht verdeckt werden), kann er die Schleife abrollen und Teilsummen mit berechnen Keine Abhängigkeiten, fügen Sie sie dann hinzu, wenn die Schleife endet. Bei Gleitkomma muss der Compiler die Operationen jedoch in der von Ihnen angeforderten Reihenfolge ausführen (Sie haben Sequenzpunkte darin, sodass der Compiler das gleiche Ergebnis garantieren muss, was eine Neuordnung nicht zulässt), sodass jede Addition stark von abhängig ist das Ergebnis des vorherigen.
Sie werden wahrscheinlich auch mehr ganzzahlige Operanden gleichzeitig in den Cache einfügen. Daher kann die Festkomma-Version die Float-Version sogar auf einer Maschine mit theoretisch höherem Durchsatz um eine Größenordnung übertreffen.
quelle
Das Hinzufügen ist viel schneller als
rand
, daher ist Ihr Programm (besonders) nutzlos.Sie müssen Leistungs-Hotspots identifizieren und Ihr Programm schrittweise ändern. Es hört sich so an, als hätten Sie Probleme mit Ihrer Entwicklungsumgebung, die zuerst gelöst werden müssen. Ist es unmöglich, Ihr Programm für ein kleines Problem auf Ihrem PC auszuführen?
Im Allgemeinen ist der Versuch von FP-Jobs mit Ganzzahlarithmetik ein Rezept für langsame.
quelle
timespec_t
oder ähnliches an. Notieren Sie die Zeit am Anfang und Ende der Schleife und nehmen Sie die Differenz. Verschieben Sie dann dierand
Datengenerierung aus der Schleife. Stellen Sie sicher, dass Ihr Algorithmus alle Daten von Arrays erhält und alle Daten in Arrays ablegt. Dadurch wird Ihr eigentlicher Algorithmus von selbst abgerufen und Setup, Malloc, Ergebnisdruck, alles andere als Taskwechsel und Interrupts werden aus Ihrer Profiling-Schleife entfernt.BIS Das ist sehr unterschiedlich. Hier sind einige Ergebnisse mit dem Gnu-Compiler (übrigens habe ich auch beim Kompilieren auf Maschinen überprüft, dass Gnu G ++ 5.4 von Xenial verdammt viel schneller ist als 4.6.3 von Linaro).
Intel i7 4700MQ Xenial
Intel i3 2370M hat ähnliche Ergebnisse
Intel (R) Celeron (R) 2955U (Acer C720 Chromebook mit Xenial)
DigitalOcean 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (läuft vertrauenswürdig)
AMD Opteron (tm) Prozessor 4122 (präzise)
Dies verwendet Code von http://pastebin.com/Kx8WGUfg als
benchmark-pc.c
Ich habe mehrere Durchgänge durchgeführt, aber dies scheint der Fall zu sein, dass die allgemeinen Zahlen gleich sind.
Eine bemerkenswerte Ausnahme scheint ALU mul vs FPU mul zu sein. Addition und Subtraktion scheinen trivial unterschiedlich zu sein.
Hier ist das Obige in Diagrammform (Klicken für volle Größe, niedriger ist schneller und vorzuziehen):
Update für @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial 64-Bit (alle Patches für 2018-03-13 angewendet) AMD Opteron (tm) Prozessor 4122 (präzises DreamHost Shared Hosting) Intel Xeon E5-2630L v2 bei 2,4 GHz (vertrauenswürdiges 64-Bit, DigitalOcean VPS)quelle
benchmark-pc
eine Kombination aus Durchsatz und Latenz gemessen? Auf Ihrem Haswell (i7 4700MQ) beträgt die Ganzzahlmultiplikation 1 pro Taktdurchsatz, 3-Takt-Latenz, aber die Ganzzahl-Addition / Sub 4 pro Taktdurchsatz, 1 Zyklus-Latenz ( agner.org/optimize ). Vermutlich gibt es eine Menge Loop-Overhead, der diese Zahlen verwässert, damit Add und Mul so nahe kommen (Long Add: 0,824088 vs. Long Mul: 1,017164). (gcc rollt standardmäßig keine Schleifen ab, außer wenn sehr niedrige Iterationszahlen vollständig abgewickelt werden).int
nurshort
undlong
? Auf Linux x86-64,short
beträgt 16 Bits (und somit Teilregister Verlangsamungen in einigen Fällen), währendlong
undlong long
sind beide 64-Bit - Typen. (Vielleicht ist es für Windows konzipiert, wo x86-64 noch 32-Bit verwendetlong
? Oder es ist für den 32-Bit-Modus konzipiert.) Unter Linux verfügt der x32-ABI über 32-Bitlong
im 64-Bit-Modus . Wenn Sie also die Bibliotheken installiert haben ,gcc -mx32
zum Compiler für ILP32 verwenden. Oder verwenden Sie einfach-m32
dielong
Zahlen und sehen Sie sie sich an .addps
Sie stattdessen xmm-Registeraddss
, um 4 FP- Adds parallel in einem Befehl auszuführen , der so schnell wie Skalar istaddss
. (Verwenden Sie-march=native
diese Option , um die Verwendung der von Ihrer CPU unterstützten Befehlssätze zuzulassen, nicht nur der SSE2-Baseline für x86-64.)Zwei Punkte zu beachten -
Moderne Hardware kann Anweisungen überlappen, parallel ausführen und neu anordnen, um die Hardware optimal zu nutzen. Außerdem hat jedes signifikante Gleitkomma-Programm wahrscheinlich auch eine signifikante Ganzzahl-Arbeit, selbst wenn es nur Indizes in Arrays, Schleifenzähler usw. berechnet. Selbst wenn Sie einen langsamen Gleitkomma-Befehl haben, kann es durchaus auf einem separaten Hardware-Bit ausgeführt werden überlappt mit einem Teil der ganzzahligen Arbeit. Mein Punkt ist, dass Ihr Gesamtprogramm möglicherweise schneller ausgeführt wird, selbst wenn die Gleitkommaanweisungen langsamer sind als die ganzzahligen, da es mehr Hardware verwenden kann.
Wie immer können Sie nur sicher sein, wenn Sie Ihr aktuelles Programm profilieren.
Der zweite Punkt ist, dass die meisten CPUs heutzutage über SIMD-Anweisungen für Gleitkommawerte verfügen, die gleichzeitig mit mehreren Gleitkommawerten arbeiten können. Zum Beispiel können Sie 4 Floats in ein einzelnes SSE-Register laden und 4 Multiplikationen parallel durchführen. Wenn Sie Teile Ihres Codes neu schreiben können, um SSE-Anweisungen zu verwenden, ist dies wahrscheinlich schneller als eine Ganzzahlversion. Visual c ++ bietet hierfür Compiler-Funktionen. Weitere Informationen finden Sie unter http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx .
quelle
Die Gleitkommaversion ist viel langsamer, wenn keine Restoperation ausgeführt wird. Da alle Additionen sequentiell sind, kann die CPU die Summierung nicht parallelisieren. Die Latenz ist kritisch. Die FPU-Additionslatenz beträgt normalerweise 3 Zyklen, während die Ganzzahladdition 1 Zyklus beträgt. Der Teiler für den Restbetreiber wird jedoch wahrscheinlich der kritische Teil sein, da er auf modernen CPUs nicht vollständig per Pipeline übertragen wird. Unter der Annahme, dass der Divide / Rest-Befehl den größten Teil der Zeit in Anspruch nimmt, ist der Unterschied aufgrund der zusätzlichen Latenz gering.
quelle
Wenn Sie keinen Code schreiben, der millionenfach pro Sekunde aufgerufen wird (z. B. das Zeichnen einer Linie zum Bildschirm in einer Grafikanwendung), ist die Ganzzahl- oder Gleitkomma-Arithmetik selten der Engpass.
Der übliche erste Schritt zu den Effizienzfragen besteht darin, Ihren Code zu profilieren, um zu sehen, wo die Laufzeit tatsächlich verbracht wird. Der Linux-Befehl hierfür lautet
gprof
.Bearbeiten:
Obwohl Sie den Strichzeichnungsalgorithmus immer mit Ganzzahlen und Gleitkommazahlen implementieren können, rufen Sie ihn häufig auf und prüfen Sie, ob er einen Unterschied macht:
http://en.wikipedia.org/wiki/Bresenham's_algorithm
quelle
Ganzzahloperationen sind heutzutage normalerweise etwas schneller als Gleitkommaoperationen. Wenn Sie also eine Berechnung mit denselben Operationen in Ganzzahl und Gleitkomma durchführen können, verwenden Sie Ganzzahl. JEDOCH sagen Sie "Dies verursacht eine Menge nerviger Probleme und fügt viel nervigen Code hinzu". Das hört sich so an, als ob Sie mehr Operationen benötigen, da Sie anstelle von Gleitkommazahlen Ganzzahlarithmetik verwenden. In diesem Fall wird Gleitkomma weil schneller ausgeführt
Sobald Sie mehr ganzzahlige Operationen benötigen, benötigen Sie wahrscheinlich viel mehr, sodass der leichte Geschwindigkeitsvorteil durch die zusätzlichen Operationen mehr als aufgefressen wird
Der Gleitkomma-Code ist einfacher, was bedeutet, dass der Code schneller geschrieben werden kann. Wenn er geschwindigkeitskritisch ist, können Sie mehr Zeit für die Optimierung des Codes aufwenden.
quelle
Ich habe einen Test durchgeführt, bei dem der Zahl nur 1 anstelle von rand () hinzugefügt wurde. Ergebnisse (auf einem x86-64) waren:
quelle
Basierend auf diesem ach so zuverlässigen "Etwas, das ich gehört habe" war die Ganzzahlberechnung früher etwa 20- bis 50-mal schneller als der Gleitkomma, und heutzutage ist sie weniger als doppelt so schnell.
quelle