Leistung eingebauter Typen:char vs. short vs. int vs. float vs. double

Leistung eingebauter Typen:char vs. short vs. int vs. float vs. double


Dies mag eine etwas dumme Frage erscheinen, aber wenn ich die Antwort von Alexandre C in dem anderen Thema sehe, bin ich neugierig zu wissen, ob es einen Leistungsunterschied mit den integrierten Typen gibt:



Normalerweise berücksichtigen wir solche Leistungsunterschiede (falls vorhanden) in unseren realen Projekten nicht, aber ich würde dies gerne zu Bildungszwecken wissen. Die allgemeinen Fragen, die gestellt werden können, sind:



  • Gibt es einen Leistungsunterschied zwischen Integralarithmetik und Gleitkommaarithmetik?


  • Welche ist schneller? Was ist der Grund dafür, schneller zu sein? Bitte erläutern Sie dies.



Antworten:


Float vs. Integer:


In der Vergangenheit konnten Gleitkommazahlen viel langsamer sein als Integer-Arithmetik. Auf modernen Computern ist dies nicht mehr wirklich der Fall (auf einigen Plattformen ist es etwas langsamer, aber wenn Sie nicht perfekten Code schreiben und für jeden Zyklus optimieren, wird der Unterschied von den anderen Ineffizienzen in Ihrem Code überschwemmt).


Auf etwas eingeschränkten Prozessoren, wie denen in High-End-Handys, ist Gleitkomma möglicherweise etwas langsamer als Integer, aber es liegt im Allgemeinen innerhalb einer Größenordnung (oder besser), solange Hardware-Gleitkomma verfügbar ist. Es ist erwähnenswert, dass sich diese Lücke ziemlich schnell schließt, da Mobiltelefone für immer allgemeinere Rechenlasten benötigt werden.


Auf sehr begrenzte Prozessoren (billige Handys und Ihr Toaster) gibt es im Allgemeinen keine Gleitkomma-Hardware, daher müssen Gleitkomma-Operationen in Software emuliert werden. Das ist langsam – ein paar Größenordnungen langsamer als Ganzzahlarithmetik.


Wie ich bereits sagte, erwarten die Leute, dass sich ihre Telefone und andere Geräte mehr und mehr wie „echte Computer“ verhalten, und Hardware-Designer verstärken schnell die FPUs, um dieser Nachfrage gerecht zu werden. Wenn Sie nicht jeden letzten Zyklus verfolgen oder Code für sehr begrenzte CPUs schreiben, die wenig oder keine Gleitkommaunterstützung haben, spielt der Leistungsunterschied für Sie keine Rolle.


Ganzzahltypen unterschiedlicher Größe:


Typischerweise CPUs arbeiten am schnellsten mit ganzen Zahlen ihrer ursprünglichen Wortgröße (mit einigen Einschränkungen bei 64-Bit-Systemen). 32-Bit-Operationen sind auf modernen CPUs oft schneller als 8- oder 16-Bit-Operationen, aber das variiert ziemlich stark zwischen den Architekturen. Denken Sie auch daran, dass Sie die Geschwindigkeit einer CPU nicht isoliert betrachten können. es ist Teil eines komplexen Systems. Auch wenn das Arbeiten mit 16-Bit-Zahlen doppelt so langsam ist wie das Arbeiten mit 32-Bit-Zahlen, können Sie doppelt so viele Daten in die Cache-Hierarchie einfügen, wenn Sie sie mit 16-Bit-Zahlen anstelle von 32-Bit darstellen. Wenn dies den Unterschied macht, ob alle Ihre Daten aus dem Cache kommen, anstatt häufige Cache-Fehlversuche zu machen, dann wird der schnellere Speicherzugriff den langsameren Betrieb der CPU übertrumpfen.


Sonstige Anmerkungen:


Die Vektorisierung kippt das Gleichgewicht weiter zugunsten schmalerer Typen (float und 8- und 16-Bit-Ganzzahlen) - Sie können mehr Operationen in einem Vektor derselben Breite ausführen. Guter Vektorcode ist jedoch schwer zu schreiben, also ist es nicht so, dass Sie diesen Vorteil ohne viel sorgfältige Arbeit erhalten.


Warum gibt es Leistungsunterschiede?


Es gibt wirklich nur zwei Faktoren, die beeinflussen, ob eine Operation auf einer CPU schnell ist oder nicht:die Schaltungskomplexität der Operation und die Benutzerforderung nach einer schnellen Operation.


(In vernünftigem Rahmen) kann jede Operation schnell durchgeführt werden, wenn die Chipdesigner bereit sind, genügend Transistoren auf das Problem zu werfen. Aber Transistoren kosten Geld (oder besser gesagt, die Verwendung vieler Transistoren macht Ihren Chip größer, was bedeutet, dass Sie weniger Chips pro Wafer und geringere Erträge erhalten, was Geld kostet), also müssen Chipdesigner abwägen, wie viel Komplexität für welche Operationen verwendet werden soll und Sie tun dies basierend auf der (wahrgenommenen) Benutzernachfrage. Grob könnten Sie daran denken, Operationen in vier Kategorien zu unterteilen:


                 high demand            low demand
high complexity FP add, multiply division
low complexity integer add popcount, hcf
boolean ops, shifts

Operationen mit hoher Nachfrage und geringer Komplexität werden auf fast jeder CPU schnell sein:Sie sind die niedrig hängende Frucht und bieten maximalen Benutzernutzen pro Transistor.


Vorgänge mit hoher Nachfrage und hoher Komplexität werden auf teuren CPUs (wie sie in Computern verwendet werden) schnell sein, da die Benutzer bereit sind, dafür zu bezahlen. Sie sind wahrscheinlich nicht bereit, zusätzliche 3 $ für Ihren Toaster zu bezahlen, um eine schnelle FP-Multiplikation zu haben, so dass billige CPUs an diesen Anweisungen knausern werden.


Operationen mit geringer Nachfrage und hoher Komplexität werden im Allgemeinen auf fast allen Prozessoren langsam sein; der Nutzen reicht einfach nicht aus, um die Kosten zu rechtfertigen.


Operationen mit geringer Nachfrage und geringer Komplexität sind schnell, wenn sich jemand die Mühe macht, darüber nachzudenken, und ansonsten nicht vorhanden.


Weiterführende Literatur:



  • Agner Fog unterhält eine schöne Website mit vielen Diskussionen über Leistungsdetails auf niedriger Ebene (und hat eine sehr wissenschaftliche Datenerfassungsmethodik, um dies zu untermauern).

  • Das Intel® 64 and IA-32 Architectures Optimization Reference Manual (PDF-Download-Link befindet sich etwas weiter unten auf der Seite) deckt ebenfalls viele dieser Probleme ab, obwohl es sich auf eine bestimmte Familie von Architekturen konzentriert.


Einige Code-Antworten



high demandlow demand high complexity FP add, multiply
division low complexity integer addpopcount, hcf
boolean ops, shifts
#include <iostream>
#include <windows.h>
using std::cout;
using std::cin;
using std::endl;
LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds;
LARGE_INTEGER Frequency;
void inline showElapsed(const char activity []) {
QueryPerformanceCounter(&EndingTime);
ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
ElapsedMicroseconds.QuadPart *= 1000000;
ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;
cout <<
activity <<
" took: " <<
ElapsedMicroseconds.QuadPart <<
"us" <<
endl;
} int main() {
cout <<
"Hallo!" <<
endl <<
endl;
QueryPerformanceFrequency(&Frequency);
const int32_t count = 1100100;
char activity[200];
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise &
Set %d 8 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int8_t *data8 = new int8_t[count];
for (int i = 0;
i <
count;
i++)
{
data8[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 8 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0;
i <
count;
i++)
{
data8[i] = i + 5;
}
showElapsed(activity);
cout <<
endl;
//-----------------------------------------------------------------------------------------//
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise &
Set %d 16 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int16_t *data16 = new int16_t[count];
for (int i = 0;
i <
count;
i++)
{
data16[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 16 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0;
i <
count;
i++)
{
data16[i] = i + 5;
}
showElapsed(activity);
cout <<
endl;
//-----------------------------------------------------------------------------------------//
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise &
Set %d 32 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int32_t *data32 = new int32_t[count];
for (int i = 0;
i <
count;
i++)
{
data32[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 32 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0;
i <
count;
i++)
{
data32[i] = i + 5;
}
showElapsed(activity);
cout <<
endl;
//-----------------------------------------------------------------------------------------//
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise &
Set %d 64 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int64_t *data64 = new int64_t[count];
for (int i = 0;
i <
count;
i++)
{
data64[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 64 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0;
i <
count;
i++)
{
data64[i] = i + 5;
}
showElapsed(activity);
cout <<
endl;
//-----------------------------------------------------------------------------------------//
getchar();
} /* My results on i7 4790k: Initialise &
Set 1100100 8 bit integers took: 444us Add 5 to 1100100 8 bit integers took: 358us Initialise &
Set 1100100 16 bit integers took: 666us Add 5 to 1100100 16 bit integers took: 359us Initialise &
Set 1100100 32 bit integers took: 870us Add 5 to 1100100 32 bit integers took: 276us Initialise &
Set 1100100 64 bit integers took: 2201us Add 5 to 1100100 64 bit integers took: 659us */