wake-up-neo.com

Leistung der integrierten Typen: Char vs. Short vs. Int. Vs. Float vs. Double

Dies mag eine etwas blöde Frage sein, aber da ich in dem anderen Thema Alexandre C reply sehe, bin ich neugierig zu wissen, ob bei den eingebauten Typen ein Leistungsunterschied besteht: 

char vs short vs int vs. floatdouble.

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

  • Gibt es einen Leistungsunterschied zwischen integraler Arithmetik und Fließkomma-Arithmetik?

  • Welche ist schneller? Was ist der Grund dafür, schneller zu sein? Bitte erkläre das.

60
Nawaz

Float vs. Integer:

In der Vergangenheit könnte der Gleitkomma-Wert wesentlich langsamer sein als die Ganzzahl-Arithmetik. Auf modernen Computern ist dies nicht mehr wirklich der Fall (es ist auf einigen Plattformen etwas langsamer, aber wenn Sie keinen perfekten Code schreiben und für jeden Zyklus optimieren, wird der Unterschied durch die anderen Ineffizienzen in Ihrem Code überlastet).

Auf etwas eingeschränkten Prozessoren, wie z. B. in High-End-Mobiltelefonen, ist der Fließpunkt möglicherweise etwas langsamer als die Ganzzahl, liegt jedoch im Allgemeinen in einer Größenordnung (oder besser), solange Hardware-Fließpunkt verfügbar ist. Es ist erwähnenswert, dass diese Lücke sich ziemlich schnell schließt, da Mobiltelefone immer mehr allgemeine Rechenarbeitslasten benötigen.

Auf sehr begrenzten Prozessoren (billige Handys und Ihr Toaster) gibt es im Allgemeinen keine Fließkomma-Hardware. Daher müssen Fließkommaoperationen in der Software emuliert werden. Dies 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-Entwickler erhöhen FPUs schnell, um diese Anforderungen zu erfüllen. Wenn Sie nicht jeden letzten Zyklus nachjagen oder Code für sehr eingeschränkte CPUs schreiben, die wenig oder keine Gleitkomma-Unterstützung haben, ist der Unterschied der Leistung für Sie egal.

Verschiedene Integer-Typen:

Normalerweise arbeiten CPUs am schnellsten mit Ganzzahlen ihrer ursprünglichen Word-Größe (mit einigen Vorbehalten bei 64-Bit-Systemen). 32-Bit-Vorgänge sind auf modernen CPUs oft schneller als 8- oder 16-Bit-Vorgänge. Dies ist jedoch bei den verschiedenen Architekturen recht unterschiedlich. Denken Sie auch daran, dass Sie die Geschwindigkeit einer CPU nicht isoliert betrachten können. es ist Teil eines komplexen Systems. Selbst wenn der Betrieb mit 16-Bit-Nummern 2x langsamer ist als mit 32-Bit-Nummern, können Sie doppelt so viele Daten in die Cache-Hierarchie einfügen, wenn Sie sie mit 16-Bit-Nummern anstelle von 32-Bit-Werten darstellen. Wenn dies den Unterschied ausmacht, dass alle Ihre Daten vom Cache kommen, anstatt häufige Cache-Fehler zu machen, wird der schnellere Speicherzugriff den langsameren Betrieb der CPU beeinträchtigen.

Weitere Hinweise:

Die Vektorisierung gibt der Waage einen weiteren Schwerpunkt auf engere 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. Es ist also nicht so, dass Sie diesen Vorteil ohne 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 Komplexität der Schaltung der Operation und die Anforderung des Benutzers nach einer schnellen Operation.

(In begründeter Weise) kann jede Operation schnell ausgeführt werden, wenn die Chipdesigner bereit sind, genügend Transistoren auf das Problem zu werfen. Transistoren kosten jedoch Geld (oder die Verwendung vieler Transistoren macht Ihren Chip größer, was bedeutet, dass Sie weniger Chips pro Wafer und geringere Erträge benötigen, was Geld kostet). Daher müssen die Chipdesigner die Komplexität für die einzelnen Operationen ausgleichen Sie tun dies basierend auf der (wahrgenommenen) Nachfrage der Benutzer. Grundsätzlich könnten Sie daran denken, die 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 hohem Bedarf und geringer Komplexität werden auf fast jeder CPU schnell erledigt: Sie sind die niedrigsten Früchte und bieten einen maximalen Nutzervorteil pro Transistor.

operationen mit hohem Bedarf und hoher Komplexität sind auf teuren CPUs (wie sie in Computern üblich sind) schnell, da Benutzer bereit sind, dafür zu zahlen. Sie sind wahrscheinlich nicht bereit, für den Toaster eine zusätzliche $ 3 zu zahlen, um eine schnelle FP -Multiplikation zu erhalten. Allerdings werden billige Prozessoren diese Anweisungen einschränken.

operationen mit geringem Bedarf und hoher Komplexität werden auf fast allen Prozessoren im Allgemeinen langsam sein. Es gibt nicht genug Nutzen, um die Kosten zu rechtfertigen.

operationen mit geringem Bedarf und geringer Komplexität sind schnell, wenn jemand darüber nachdenkt und ansonsten nicht vorhanden ist.

Weiterführende Literatur:

  • Agner Fog unterhält eine Nice Website mit vielen Diskussionen über Leistungsdetails auf niedriger Ebene (und verfügt über eine sehr wissenschaftliche Methodik zur Datensammlung, um dies zu sichern).
  • Das Referenzhandbuch für die Intel® 64- und IA-32-Architekturen-Optimierung (Der PDF-Download-Link befindet sich zum Teil auf der Seite) deckt viele dieser Probleme ab, obwohl er sich auf eine bestimmte Familie von Architekturen konzentriert.
108
Stephen Canon

Absolut.

Erstens hängt es natürlich vollständig von der jeweiligen CPU-Architektur ab.

Integral- und Fließkommatypen werden jedoch sehr unterschiedlich gehandhabt, daher ist fast immer Folgendes der Fall:

  • für einfache Operationen sind Integraltypen fast . Zum Beispiel hat die Integer-Addition oft nur eine Latenzzeit eines einzelnen Zyklus, und die Multiplikation der Integer-Zahl beträgt typischerweise etwa 2-4 Zyklen (IIRC).
  • Gleitpunkttypen werden normalerweise langsamer ausgeführt. Bei heutigen CPUs weisen sie jedoch einen hervorragenden Durchsatz auf, und jede Gleitkommaeinheit kann normalerweise eine Operation pro Zyklus zurückziehen, was zu demselben (oder einem ähnlichen) Durchsatz wie bei Ganzzahloperationen führt. Die Latenz ist jedoch im Allgemeinen schlechter. Die Hinzufügung von Fließkommazahlen hat oft eine Latenz von etwa 4 Zyklen (vs 1 für Ints).
  • bei einigen komplexen Vorgängen ist die Situation anders oder sogar umgekehrt. Zum Beispiel kann eine Division auf FP weniger Latenz als für ganze Zahlen haben, einfach weil die Operation in beiden Fällen komplex zu implementieren ist, sie ist jedoch häufiger für FP - Werte nützlich, also eher Mühe (und Transistoren) können diesen Fall optimieren.

Bei einigen CPUs sind Doppelte möglicherweise wesentlich langsamer als Floats. Bei einigen Architekturen gibt es keine dedizierte Hardware für Doubles. Daher werden sie durch das Passieren von zwei Float-großen Chunks gehandhabt, was einen schlechteren Durchsatz und eine doppelt so lange Latenz ergibt. Bei anderen Geräten (z. B. der x86-FPU) werden beide Typen in das gleiche interne Format (80-Bit-Gleitkommazahl, im Fall von x86) konvertiert. Die Leistung ist also identisch. Auf anderen Plattformen haben sowohl Float als auch Double die richtige Hardwareunterstützung. Da Float jedoch weniger Bits enthält, kann dies etwas schneller erfolgen, was die Latenz relativ zu Double-Vorgängen reduziert.

Haftungsausschluss: Alle genannten Zeiten und Merkmale werden nur aus dem Speicher gezogen. Ich habe nicht nachgesehen, daher könnte es falsch sein. ;)

Bei verschiedenen Ganzzahltypen variiert die Antwort je nach CPU-Architektur stark. Die x86-Architektur muss aufgrund ihrer langen verschlungenen Geschichte nativ 8, 16, 32 (und heute 64) Operationen unterstützen, und im Allgemeinen sind sie alle gleich schnell (sie verwenden im Wesentlichen dieselbe Hardware und nur null.) die oberen Bits nach Bedarf aus).

Auf anderen CPUs können Datentypen, die kleiner als eine int sind, für das Laden/Speichern jedoch teurer sein (das Schreiben eines Bytes in den Speicher muss möglicherweise durch Laden des gesamten 32-Bit-Words, in dem es sich befindet, und anschließende Bit-Maskierung zum Aktualisieren durchgeführt werden das einzelne Byte in einem Register, und schreiben Sie dann das gesamte Wort zurück). In ähnlicher Weise müssen einige CPUs für Datentypen, die größer als int sind, die Operation in zwei Hälften teilen, wobei die untere und die obere Hälfte getrennt geladen/gespeichert/berechnet werden.

Aber bei x86 ist die Antwort, dass es meistens keine Rolle spielt. Aus historischen Gründen muss die CPU für jeden Datentyp eine recht robuste Unterstützung haben. Der einzige Unterschied, den Sie wahrscheinlich bemerken werden, ist, dass Gleitkommaoperationen mehr Latenz haben (aber einen ähnlichen Durchsatz, daher sind sie nicht langsamer per se, zumindest wenn Sie Ihren Code richtig schreiben).

9
jalf

Ich glaube nicht, dass irgendjemand die ganzzahligen Promotionsregeln erwähnt hat. In Standard C/C++ kann keine Operation für einen Typ ausgeführt werden, der kleiner als int ist. Wenn char oder short auf der aktuellen Plattform kleiner als int sind, werden sie implizit auf int (das ist eine Hauptursache für Fehler) heraufgestuft. Der Complier ist für diese implizite Promotion erforderlich. Es gibt keinen Weg daran vorbei, ohne den Standard zu verletzen.

Die Integer-Promotions bedeuten, dass keine Operation (Addition, bitweise, logisch usw.) in der Sprache auf einem kleineren Integer-Typ als int ausgeführt werden kann. Daher sind Operationen für char/short/int im Allgemeinen gleich schnell, da die ersteren zu letzteren befördert werden.

Zusätzlich zu den ganzzahligen Beförderungen gibt es die "üblichen arithmetischen Konvertierungen", was bedeutet, dass C bestrebt ist, beide Operanden vom gleichen Typ zu machen, wobei einer von ihnen in den größeren von beiden konvertiert wird, falls sie unterschiedlich sind.

Die CPU kann jedoch verschiedene Lade-/Speichervorgänge auf Ebene 8, 16, 32 usw. ausführen. Bei 8- und 16-Bit-Architekturen bedeutet dies oft, dass 8- und 16-Bit-Typen trotz Integer-Promotions schneller sind. Bei einer 32-Bit-CPU könnte dies tatsächlich bedeuten, dass die kleineren Typen langsamer sind, weil sie alles in 32-Bit-Blöcken ordentlich ausrichten möchten. 32-Bit-Compiler optimieren in der Regel die Geschwindigkeit und ordnen kleinere Integer-Typen in einem größeren Speicherbereich als angegeben zu.

Im Allgemeinen benötigen die kleineren Integer-Typen natürlich weniger Platz als die größeren. Wenn Sie also die Größe von RAM optimieren möchten, sollten Sie dies vorziehen.

6
Lundin

Gibt es einen Leistungsunterschied zwischen integraler Arithmetik und Fließkomma-Arithmetik?

Ja. Dies ist jedoch sehr plattform- und CPU-spezifisch. Verschiedene Plattformen können unterschiedliche Rechenoperationen mit unterschiedlichen Geschwindigkeiten ausführen.

Davon abgesehen war die Antwort etwas konkreter. pow() ist eine Allzweckroutine, die mit doppelten Werten arbeitet. Durch das Zuführen von ganzzahligen Werten wird immer noch die gesamte Arbeit erledigt, die für die Verarbeitung von nicht ganzzahligen Exponenten erforderlich wäre. Durch die direkte Multiplikation wird ein Großteil der Komplexität umgangen, und hier kommt die Geschwindigkeit ins Spiel. Dies ist wirklich kein Thema (so sehr) für verschiedene Typen, sondern das Umgehen einer großen Menge an komplexem Code, der erforderlich ist, um die pow-Funktion mit einem beliebigen Exponenten auszuführen.

2
Reed Copsey

Die erste Antwort oben ist großartig und ich habe einen kleinen Block davon in das folgende Duplikat kopiert (da dies der Ort war, an dem ich zuerst gelandet bin).

Sind "char" und "small int" langsamer als "int"?

Ich möchte den folgenden Code anbieten, der Profile für die Zuweisung, Initialisierung und einige Arithmetik der verschiedenen Integer-Größen enthält:

#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
*/

Meine Ergebnisse in MSVC auf i7 4790k:

Initialisieren und setzen 1100100 8-Bit-Integer-Werte: 444us
Addiere 5 bis 1100100 8-Bit-Ganzzahlen: 358us

Initialize & Set 1100100 16-Bit-Integer-Werte: 666us
Addiere 5 bis 1100100 16-Bit-Ganzzahlen: 359us

Initialize & Set 1100100 Für 32-Bit-Integerwerte: 870us
Addiere 5 bis 1100100: 32-Bit-Integer-Werte: 276us

Initialize & Set 1100100 Für 64-Bit-Ganzzahlen wurde Folgendes verwendet: 2201us
Fügen Sie 5 bis 1100100 64-Bit-Ganzzahlen hinzu: 659us

2
Researcher

Abhängig von der Zusammensetzung des Prozessors und der Plattform.

Plattformen mit einem Fließkomma-Coprozessor sind möglicherweise langsamer als die integrale Arithmetik, da Werte zum und vom Coprozessor übertragen werden müssen. 

Wenn die Gleitkomma-Verarbeitung im Kern des Prozessors liegt, kann die Ausführungszeit vernachlässigbar sein.

Wenn die Gleitkommaberechnungen von der Software emuliert werden, ist die Integralarithmetik schneller.

Im Zweifelsfall Profil.

Sorgen Sie dafür, dass die Programmierung vor der Optimierung richtig und robust funktioniert.

1
Thomas Matthews

Es gibt sicherlich einen Unterschied zwischen Fließkomma- und Ganzzahlarithmetik. Abhängig von der spezifischen Hardware und den Mikrobefehlen der CPU erhalten Sie eine unterschiedliche Leistung und/oder Präzision. Gute Google-Begriffe für die genauen Beschreibungen (ich weiß es auch nicht genau):

FPU x87 MMX SSE

In Bezug auf die Größe der Ganzzahlen ist es am besten, die Plattform-/Architektur-Word-Größe (oder die doppelte Größe) zu verwenden, die auf x86_64 auf int32_t und auf x86_64 int64_t zurückzuführen ist. SOme-Prozessoren verfügen möglicherweise über intrinsische Anweisungen, die mehrere dieser Werte gleichzeitig verarbeiten (wie SSE (Fließkommazahl) und MMX), wodurch parallele Additionen oder Multiplikationen beschleunigt werden.

0
rubenvb

Im Allgemeinen ist die Ganzzahl-Mathematik schneller als die Fließkomma-Mathematik. Dies liegt daran, dass die Ganzzahl-Mathematik einfachere Berechnungen erfordert. Bei den meisten Operationen handelt es sich jedoch um weniger als ein Dutzend Uhren. Nicht Millis, Micros, Nanos oder Zecken; Uhren Diejenigen, die in modernen Kernen zwischen 2-3 Milliarden Mal pro Sekunde vorkommen. Seit dem 486 haben viele Kerne einen Satz von Floating-Point-Processing-Units oder FPUs, die fest verdrahtet sind, um Fließkomma-Arithmetik effizient und häufig parallel zur CPU auszuführen. 

Fließkomma-Berechnungen sind zwar technisch langsamer, aber dennoch so schnell, dass jeder Versuch, die Differenz zu messen, mehr Fehler im Timing-Mechanismus und in der Thread-Planung hätte, als tatsächlich für die Berechnung erforderlich ist. Verwenden Sie Ints, wenn Sie können, aber verstehen Sie, wenn Sie nicht können, und sorgen Sie sich nicht zu sehr um die relative Berechnungsgeschwindigkeit.

0
KeithS

Nein nicht wirklich. Dies hängt natürlich von der CPU und dem Compiler ab, aber der Leistungsunterschied ist in der Regel vernachlässigbar - wenn überhaupt.

0
Puppy