Gleitkomma-Ganzzahl-Berechnungen auf moderner Hardware

100

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

Maxpenguin
quelle
8
Was Sie jetzt als Test haben, ist trivial. Es gibt wahrscheinlich auch sehr wenig Unterschied in der Baugruppe ( addlersetzt durch faddzum 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 Compiler fixed_pointmitteilen, 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.
GManNickG
1
Es gibt immer noch viele Architekturen ohne dedizierte Gleitkomma-Hardware. Einige Tags, die die Systeme erläutern, die Sie interessieren, helfen Ihnen dabei, bessere Antworten zu erhalten.
Carl Norum
3
Ich glaube, die Hardware in meinem HTC Hero (Android) hat keine FPU, aber die Hardware in Google NexusOne (Android). Was ist dein Ziel? Desktop / Server-PCs? Netbooks (möglicher Arm + Linux)? Telefone?
SteelBytes
5
Wenn Sie schnelles FP auf x86 möchten, versuchen Sie, mit Optimierung und SSE-Codegenerierung zu kompilieren. SSE (welche Version auch immer) kann in einem einzigen Zyklus mindestens float addieren, subtrahieren und multiplizieren. Divide-, Mod- und höhere Funktionen sind immer langsam. Beachten Sie auch, dass dies floatden Geschwindigkeitsschub erhält, dies aber normalerweise doublenicht tut.
Mike D.
1
Die Festkomma-Ganzzahl nähert sich der FP an, indem mehrere Ganzzahloperationen verwendet werden, um ein Überlaufen der Ergebnisse zu verhindern. Das ist fast immer langsamer als nur die Verwendung der extrem leistungsfähigen FPUs, die in modernen Desktop-CPUs zu finden sind. Beispiel: MAD, der Festkomma-MP3-Decoder, ist langsamer als libmpg123, und obwohl es für einen Festkomma-Decoder von guter Qualität ist, weist libmpg123 immer noch weniger Rundungsfehler auf. wezm.net/technical/2008/04/mp3-decoder-libraries-compared für Benchmarks auf einem PPC G5.
Peter Cordes

Antworten:

35

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.

Dan
quelle
8
Und auf einigen Prozessoren kann FP-Mathematik schneller sein als Ganzzahl-Mathematik. Der Alpha-Prozessor hatte einen FP-Divide-Befehl, aber keinen Integer-Befehl, daher musste die Integer-Division in Software durchgeführt werden.
Gabe
Wird SSEx auch die Arithmetik mit doppelter Genauigkeit beschleunigen? Es tut mir leid, ich bin nicht allzu vertraut mit SSE
Johannes Schaub - litb
1
@ JohannesSchaub-litb: SSE2 (Baseline für x86-64) hat double-präzisions-FP gepackt . Mit nur zwei 64-Bit- doubles pro Register ist die potenzielle Beschleunigung geringer als floatbei Code, der gut vektorisiert. Skalieren floatund doubleverwenden Sie XMM-Register auf x86-64, wobei nur x87 verwendet wird long 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-Byte xmm0..15anstelle von 8 -Byte mm0..7und moderne CPUs haben einen schlechteren MMX als SSE-Durchsatz.)
Peter Cordes
1
MMX- und SSE * / AVX2-Integer-Anweisungen konkurrieren jedoch um dieselben Ausführungseinheiten, sodass die gleichzeitige Verwendung beider Anweisungen fast nie sinnvoll ist. Verwenden Sie einfach die breiteren XMM / YMM-Versionen, um mehr Arbeit zu erledigen. Die gleichzeitige Verwendung von SIMD Integer und FP konkurriert um dieselben Register, aber x86-64 verfügt über 16 davon. Aufgrund des Gesamtdurchsatzes können Sie jedoch nicht doppelt so viel Arbeit erledigen, wenn Sie ganzzahlige und FP-Ausführungseinheiten parallel verwenden.
Peter Cordes
49

Zum Beispiel (kleinere Zahlen sind schneller),

64-Bit Intel Xeon X5550 bei 2,67 GHz, gcc 4.1.2 -O3

short add/sub: 1.005460 [0]
short mul/div: 3.926543 [0]
long add/sub: 0.000000 [0]
long mul/div: 7.378581 [0]
long long add/sub: 0.000000 [0]
long long mul/div: 7.378593 [0]
float add/sub: 0.993583 [0]
float mul/div: 1.821565 [0]
double add/sub: 0.993884 [0]
double mul/div: 1.988664 [0]

32-Bit-Dual-Core-AMD-Opteron (tm) -Prozessor 265 bei 1,81 GHz, gcc 3.4.6 -O3

short add/sub: 0.553863 [0]
short mul/div: 12.509163 [0]
long add/sub: 0.556912 [0]
long mul/div: 12.748019 [0]
long long add/sub: 5.298999 [0]
long long mul/div: 20.461186 [0]
float add/sub: 2.688253 [0]
float mul/div: 4.683886 [0]
double add/sub: 2.700834 [0]
double mul/div: 4.646755 [0]

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:

#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>

double
mygettime(void) {
# ifdef _WIN32
  struct _timeb tb;
  _ftime(&tb);
  return (double)tb.time + (0.001 * (double)tb.millitm);
# else
  struct timeval tv;
  if(gettimeofday(&tv, 0) < 0) {
    perror("oops");
  }
  return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}

template< typename Type >
void my_test(const char* name) {
  Type v  = 0;
  // Do not use constants or repeating values
  //  to avoid loop unroll optimizations.
  // All values >0 to avoid division by 0
  // Perform ten ops/iteration to reduce
  //  impact of ++i below on measurements
  Type v0 = (Type)(rand() % 256)/16 + 1;
  Type v1 = (Type)(rand() % 256)/16 + 1;
  Type v2 = (Type)(rand() % 256)/16 + 1;
  Type v3 = (Type)(rand() % 256)/16 + 1;
  Type v4 = (Type)(rand() % 256)/16 + 1;
  Type v5 = (Type)(rand() % 256)/16 + 1;
  Type v6 = (Type)(rand() % 256)/16 + 1;
  Type v7 = (Type)(rand() % 256)/16 + 1;
  Type v8 = (Type)(rand() % 256)/16 + 1;
  Type v9 = (Type)(rand() % 256)/16 + 1;

  double t1 = mygettime();
  for (size_t i = 0; i < 100000000; ++i) {
    v += v0;
    v -= v1;
    v += v2;
    v -= v3;
    v += v4;
    v -= v5;
    v += v6;
    v -= v7;
    v += v8;
    v -= v9;
  }
  // Pretend we make use of v so compiler doesn't optimize out
  //  the loop completely
  printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1);
  t1 = mygettime();
  for (size_t i = 0; i < 100000000; ++i) {
    v /= v0;
    v *= v1;
    v /= v2;
    v *= v3;
    v /= v4;
    v *= v5;
    v /= v6;
    v *= v7;
    v /= v8;
    v *= v9;
  }
  // Pretend we make use of v so compiler doesn't optimize out
  //  the loop completely
  printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1);
}

int main() {
  my_test< short >("short");
  my_test< long >("long");
  my_test< long long >("long long");
  my_test< float >("float");
  my_test< double >("double");

  return 0;
}
vladr
quelle
8
Warum hast du Mult und Div gemischt? Sollte es nicht interessant sein, wenn mult vielleicht (oder erwartungsgemäß?) Viel schneller ist als div?
Kyss Tao
13
Die Multiplikation ist sowohl in Ganzzahl- als auch in Gleitkommafällen viel schneller als die Division. Die Leistung der Division hängt auch von der Größe der Zahlen ab. Ich gehe normalerweise davon aus, dass die Teilung ~ 15 mal langsamer ist.
Sogartar
4
pastebin.com/Kx8WGUfg Ich habe Ihren Benchmark genommen und jede Operation in eine eigene Schleife aufgeteilt und hinzugefügt volatile, um sicherzugehen. Unter Win64 wird die FPU nicht verwendet und MSVC generiert keinen Code dafür. Daher werden dort mulssund divssXMM-Anweisungen kompiliert, die 25-mal schneller sind als die FPU in Win32. Testmaschine ist Core i5 M 520 bei 2,40 GHz
James Dunne
4
@JamesDunne sei nur vorsichtig, denn fp ops verreichen sehr schnell entweder 0 oder +/- inf, was von bestimmten fpu-Implementierungen (theoretisch) als Sonderfall / Fastpatheed behandelt werden kann oder nicht.
Vladr
3
Dieser "Benchmark" weist keine Datenparallelität für die Ausführung außerhalb der Reihenfolge auf, da jede Operation mit demselben Akkumulator ausgeführt wird ( v). Bei neueren Intel-Designs ist Divide überhaupt nicht per Pipeline ( divss/ divpshat eine Latenz von 10 bis 14 Zyklen und den gleichen gegenseitigen Durchsatz). mulssDie 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).
Peter Cordes
23

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.

Ben Voigt
quelle
4
+1, um darauf hinzuweisen, wie naive Benchmarks aufgrund ungerollter konstanter ganzzahliger Operationen 0-Zeit-Schleifen ergeben können. Darüber hinaus kann der Compiler die Schleife (Ganzzahl oder FP) vollständig verwerfen, wenn das Ergebnis nicht tatsächlich verwendet wird.
Vladr
Die Schlussfolgerung dazu lautet: Man muss eine Funktion aufrufen, die die Schleifenvariable als Argument hat. Da ich denke, dass kein Compiler sehen kann, dass die Funktion nichts tut und dass der Aufruf ignoriert werden kann. Da es einen Anrufaufwand gibt, sind nur die Zeitunterschiede == (Gleitzeit - Ganzzahlzeit) signifikant.
GameAlchemist
@GameAlchemist: Viele Compiler eliminieren Aufrufe leerer Funktionen als Nebeneffekt von Inlining. Sie müssen sich bemühen, dies zu verhindern.
Ben Voigt
Das OP klang, als würde er über die Verwendung von Ganzzahlen für Dinge sprechen, bei denen FP natürlicher passt, sodass mehr Ganzzahlcode erforderlich wäre, um das gleiche Ergebnis wie der FP-Code zu erzielen. In diesem Fall verwenden Sie einfach FP. Auf Hardware mit einer FPU (z. B. einer Desktop-CPU) sind MP3-Decoder mit fester Ganzzahl langsamer (und etwas mehr Rundungsfehler) als Gleitkomma-Decoder. Festpunktimplementierungen von Codecs sind hauptsächlich für die Ausführung auf abgespeckten ARM-CPUs ohne FP-Hardware vorhanden, nur für langsam emulierte FP.
Peter Cordes
ein Beispiel für den ersten Punkt: auf x86-64 mit AVX-512 gibt es nur 16 GP - Register , aber 32 ZMM - Register so Skalar Gleitpunktarithmetik kann schneller sein
phuclv
18

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.

Kartoffelklatsche
quelle
Ja, ebenso wie die Konvertierung von einer Rand-Ganzzahl in einen Float in der Gleitkomma-Version. Irgendwelche Ideen, wie man das besser testen kann?
Maxpenguin
1
Wenn Sie versuchen, die Geschwindigkeit zu profilieren, schauen Sie sich POSIXs timespec_toder ähnliches an. Notieren Sie die Zeit am Anfang und Ende der Schleife und nehmen Sie die Differenz. Verschieben Sie dann die randDatengenerierung 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.
Mike D.
3
@maxpenguin: Die Frage ist, was Sie testen. Artem hat angenommen, dass Sie Grafiken erstellen. Carl überlegte, ob Sie sich auf einer eingebetteten Plattform ohne FP befinden. Ich nahm an, Sie programmieren die Wissenschaft für einen Server. Sie können keine Benchmarks verallgemeinern oder "schreiben". Benchmarks werden anhand der tatsächlichen Arbeit Ihres Programms ermittelt. Eine Sache, die ich Ihnen sagen kann, ist, dass es nicht "im Wesentlichen die gleiche Geschwindigkeit" bleibt, wenn Sie das leistungskritische Element in Ihrem Programm berühren, was auch immer das ist.
Potatoswatter
guter Punkt und gute Antwort. 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 danke für deine Hilfe.
Maxpenguin
18

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

short add: 0.822491
short sub: 0.832757
short mul: 1.007533
short div: 3.459642
long add: 0.824088
long sub: 0.867495
long mul: 1.017164
long div: 5.662498
long long add: 0.873705
long long sub: 0.873177
long long mul: 1.019648
long long div: 5.657374
float add: 1.137084
float sub: 1.140690
float mul: 1.410767
float div: 2.093982
double add: 1.139156
double sub: 1.146221
double mul: 1.405541
double div: 2.093173

Intel i3 2370M hat ähnliche Ergebnisse

short add: 1.369983
short sub: 1.235122
short mul: 1.345993
short div: 4.198790
long add: 1.224552
long sub: 1.223314
long mul: 1.346309
long div: 7.275912
long long add: 1.235526
long long sub: 1.223865
long long mul: 1.346409
long long div: 7.271491
float add: 1.507352
float sub: 1.506573
float mul: 2.006751
float div: 2.762262
double add: 1.507561
double sub: 1.506817
double mul: 1.843164
double div: 2.877484

Intel (R) Celeron (R) 2955U (Acer C720 Chromebook mit Xenial)

short add: 1.999639
short sub: 1.919501
short mul: 2.292759
short div: 7.801453
long add: 1.987842
long sub: 1.933746
long mul: 2.292715
long div: 12.797286
long long add: 1.920429
long long sub: 1.987339
long long mul: 2.292952
long long div: 12.795385
float add: 2.580141
float sub: 2.579344
float mul: 3.152459
float div: 4.716983
double add: 2.579279
double sub: 2.579290
double mul: 3.152649
double div: 4.691226

DigitalOcean 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (läuft vertrauenswürdig)

short add: 1.094323
short sub: 1.095886
short mul: 1.356369
short div: 4.256722
long add: 1.111328
long sub: 1.079420
long mul: 1.356105
long div: 7.422517
long long add: 1.057854
long long sub: 1.099414
long long mul: 1.368913
long long div: 7.424180
float add: 1.516550
float sub: 1.544005
float mul: 1.879592
float div: 2.798318
double add: 1.534624
double sub: 1.533405
double mul: 1.866442
double div: 2.777649

AMD Opteron (tm) Prozessor 4122 (präzise)

short add: 3.396932
short sub: 3.530665
short mul: 3.524118
short div: 15.226630
long add: 3.522978
long sub: 3.439746
long mul: 5.051004
long div: 15.125845
long long add: 4.008773
long long sub: 4.138124
long long mul: 5.090263
long long div: 14.769520
float add: 6.357209
float sub: 6.393084
float mul: 6.303037
float div: 17.541792
double add: 6.415921
double sub: 6.342832
double mul: 6.321899
double div: 15.362536

Dies verwendet Code von http://pastebin.com/Kx8WGUfg alsbenchmark-pc.c

g++ -fpermissive -O3 -o benchmark-pc 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):

Diagramm der obigen Daten

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)
    short add: 0.773049
    short sub: 0.789793
    short mul: 0.960152
    short div: 3.273668
      int add: 0.837695
      int sub: 0.804066
      int mul: 0.960840
      int div: 3.281113
     long add: 0.829946
     long sub: 0.829168
     long mul: 0.960717
     long div: 5.363420
long long add: 0.828654
long long sub: 0.805897
long long mul: 0.964164
long long div: 5.359342
    float add: 1.081649
    float sub: 1.080351
    float mul: 1.323401
    float div: 1.984582
   double add: 1.081079
   double sub: 1.082572
   double mul: 1.323857
   double div: 1.968488
AMD Opteron (tm) Prozessor 4122 (präzises DreamHost Shared Hosting)
    short add: 1.235603
    short sub: 1.235017
    short mul: 1.280661
    short div: 5.535520
      int add: 1.233110
      int sub: 1.232561
      int mul: 1.280593
      int div: 5.350998
     long add: 1.281022
     long sub: 1.251045
     long mul: 1.834241
     long div: 5.350325
long long add: 1.279738
long long sub: 1.249189
long long mul: 1.841852
long long div: 5.351960
    float add: 2.307852
    float sub: 2.305122
    float mul: 2.298346
    float div: 4.833562
   double add: 2.305454
   double sub: 2.307195
   double mul: 2.302797
   double div: 5.485736
Intel Xeon E5-2630L v2 bei 2,4 GHz (vertrauenswürdiges 64-Bit, DigitalOcean VPS)
    short add: 1.040745
    short sub: 0.998255
    short mul: 1.240751
    short div: 3.900671
      int add: 1.054430
      int sub: 1.000328
      int mul: 1.250496
      int div: 3.904415
     long add: 0.995786
     long sub: 1.021743
     long mul: 1.335557
     long div: 7.693886
long long add: 1.139643
long long sub: 1.103039
long long mul: 1.409939
long long div: 7.652080
    float add: 1.572640
    float sub: 1.532714
    float mul: 1.864489
    float div: 2.825330
   double add: 1.535827
   double sub: 1.535055
   double mul: 1.881584
   double div: 2.777245
MrMesees
quelle
gcc5 vektorisiert vielleicht etwas automatisch, was gcc4.6 nicht tat? Wird benchmark-pceine 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).
Peter Cordes
Und übrigens, warum testet es nicht intnur shortund long? Auf Linux x86-64, shortbeträgt 16 Bits (und somit Teilregister Verlangsamungen in einigen Fällen), während longund long longsind beide 64-Bit - Typen. (Vielleicht ist es für Windows konzipiert, wo x86-64 noch 32-Bit verwendet long? Oder es ist für den 32-Bit-Modus konzipiert.) Unter Linux verfügt der x32-ABI über 32-Bit longim 64-Bit-Modus . Wenn Sie also die Bibliotheken installiert haben , gcc -mx32zum Compiler für ILP32 verwenden. Oder verwenden Sie einfach -m32die longZahlen und sehen Sie sie sich an .
Peter Cordes
Und Sie sollten wirklich prüfen, ob Ihr Compiler irgendetwas automatisch vektorisiert hat. Beispiel: Verwenden addpsSie stattdessen xmm-Register addss, um 4 FP- Adds parallel in einem Befehl auszuführen , der so schnell wie Skalar ist addss. (Verwenden Sie -march=nativediese Option , um die Verwendung der von Ihrer CPU unterstützten Befehlssätze zuzulassen, nicht nur der SSE2-Baseline für x86-64.)
Peter Cordes
@cincodenada Bitte lassen Sie die Diagramme mit den vollen 15 oben, da dies die Leistung veranschaulicht.
MrMesees
@ PeterCordes Ich werde versuchen, morgen zu suchen, danke für Ihre Sorgfalt.
MrMesees
7

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 .

jcoder
quelle
Man sollte beachten, dass unter Win64 die FPU-Anweisungen nicht mehr vom MSVC-Compiler generiert werden. Gleitkomma verwendet dort immer SIMD-Anweisungen. Dies führt zu einer großen Geschwindigkeitsdiskrepanz zwischen Win32 und Win64 in Bezug auf Flops.
James Dunne
5

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.

Goran D.
quelle
4

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

Artem Sokolov
quelle
2
Wissenschaftliche Anwendungen verwenden FP. Der einzige Vorteil von FP ist, dass die Präzision skalierungsinvariant ist. Es ist wie eine wissenschaftliche Notation. Wenn Sie die Skalierung der Zahlen bereits kennen (z. B. dass die Zeilenlänge eine Anzahl von Pixeln ist), wird FP vermieden. Aber bevor Sie die Linie zeichnen, ist das nicht wahr.
Potatoswatter
4

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.

gnasher729
quelle
Hier wird viel wild spekuliert, ohne die sekundären Effekte der Hardware zu berücksichtigen, die häufig die Rechenzeit dominieren. Kein schlechter Ausgangspunkt, aber er muss für jede einzelne Anwendung per Profiling überprüft und nicht als Evangelium gelehrt werden.
Ben Voigt
3

Ich habe einen Test durchgeführt, bei dem der Zahl nur 1 anstelle von rand () hinzugefügt wurde. Ergebnisse (auf einem x86-64) waren:

  • kurz: 4.260s
  • int: 4.020s
  • lang lang: 3.350s
  • float: 7.330s
  • doppelt: 7.210s
dan04
quelle
1
Quelle, Kompilierungsoptionen und Timing-Methode? Ich bin ein bisschen überrascht von den Ergebnissen.
GManNickG
Gleiche Schleife wie OP, wobei "rand ()% 365" durch "1" ersetzt wird. Keine Optimierung. Benutzerzeit ab Befehl "Zeit".
Dan04
13
"Keine Optimierung" ist der Schlüssel. Sie profilieren nie mit deaktivierter Optimierung, sondern immer im "Release" -Modus.
Dean Harding
2
In diesem Fall erzwingt das Ausschalten der Optimierung jedoch das Auftreten der Operation und erfolgt absichtlich - die Schleife dient dazu, die Zeit auf einen angemessenen Messmaßstab zu erweitern. Durch Verwendung der Konstante 1 werden die Kosten für rand () entfernt. Ein ausreichend intelligenter Optimierungs-Compiler würde 1 100.000.000 Mal ohne Ausweg aus der Schleife hinzufügen und einfach 100000000 in einem einzigen Vorgang hinzufügen. Das umgeht den ganzen Zweck, nicht wahr?
Stan Rogers
7
@Stan, machen Sie die Variable flüchtig. Sogar ein intelligenter Optimierungs-Compiler sollte dann die mehreren Operationen berücksichtigen.
Vladr
0

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.

James Curran
quelle
1
Bitte überlegen Sie sich dies noch einmal und bieten Sie mehr als nur eine Meinung (insbesondere angesichts der Tatsache, dass die Meinung angesichts der gesammelten Fakten zu fliegen scheint)
MrMesees
1
@ MrMesees Obwohl diese Antwort nicht besonders nützlich ist, würde ich sagen, dass sie mit den von Ihnen durchgeführten Tests übereinstimmt. Und die historischen Trivia sind wahrscheinlich auch in Ordnung.
Jonatan Öström
Als jemand, der früher mit 286 gearbeitet hat, kann ich das bestätigen; "Ja, waren sie!"
David H Parry