Der Compiler soll Assembler (und letztendlich Maschinencode) für eine Maschine erzeugen, und im Allgemeinen versucht C ++, mit dieser Maschine einverstanden zu sein.
Sympathie für die zugrunde liegende Maschine bedeutet ungefähr: Es ist einfach, C ++ - Code zu schreiben, der effizient auf die Vorgänge abgebildet wird, die die Maschine schnell ausführen kann. Daher möchten wir den Zugriff auf die Datentypen und Vorgänge ermöglichen, die auf unserer Hardwareplattform schnell und "natürlich" sind.
Betrachten Sie konkret eine bestimmte Maschinenarchitektur. Nehmen wir die aktuelle Intel x86-Familie.
Das Softwareentwicklerhandbuch für Intel® 64- und IA-32-Architekturen, Band 1 ( Link ), Abschnitt 3.4.1, lautet:
Die 32-Bit-Universalregister EAX, EBX, ECX, EDX, ESI, EDI, EBP und ESP enthalten die folgenden Elemente:
• Operanden für logische und arithmetische Operationen
• Operanden für Adressberechnungen
• Speicherzeiger
Wir möchten, dass der Compiler diese EAX-, EBX- usw. Register verwendet, wenn er einfache C ++ - Ganzzahlarithmetik kompiliert. Das heißt, wenn ich ein deklariere int
, sollte es mit diesen Registern kompatibel sein, damit ich sie effizient nutzen kann.
Die Register haben immer die gleiche Größe (hier 32 Bit), daher sind meine int
Variablen immer auch 32 Bit. Ich verwende dasselbe Layout (Little-Endian), damit ich nicht jedes Mal eine Konvertierung durchführen muss, wenn ich einen Variablenwert in ein Register lade oder ein Register wieder in eine Variable speichere.
Mit godbolt können wir genau sehen, was der Compiler für einen trivialen Code tut:
int square(int num) {
return num * num;
}
Kompiliert (mit GCC 8.1 und der -fomit-frame-pointer -O3
Einfachheit halber) zu:
square(int):
imul edi, edi
mov eax, edi
ret
das heisst:
- Der
int num
Parameter wurde im Register EDI übergeben, was bedeutet, dass es genau die Größe und das Layout ist, die Intel für ein natives Register erwartet. Die Funktion muss nichts konvertieren
- Die Multiplikation ist eine einzelne Anweisung (
imul
), die sehr schnell ist
- Die Rückgabe des Ergebnisses ist lediglich eine Frage des Kopierens in ein anderes Register (der Anrufer erwartet, dass das Ergebnis in EAX abgelegt wird).
Bearbeiten: Wir können einen relevanten Vergleich hinzufügen, um den Unterschied anhand eines nicht nativen Layouts zu zeigen. Der einfachste Fall ist das Speichern von Werten in einer anderen als der nativen Breite.
Mit Godbolt können wir eine einfache native Multiplikation vergleichen
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
mit dem entsprechenden Code für eine nicht standardmäßige Breite
struct pair {
unsigned x : 31;
unsigned y : 31;
};
unsigned mult (pair p)
{
return p.x*p.y;
}
mult(pair):
mov eax, edi
shr rdi, 32
and eax, 2147483647
and edi, 2147483647
imul eax, edi
ret
Alle zusätzlichen Anweisungen betreffen die Konvertierung des Eingabeformats (zwei vorzeichenlose 31-Bit-Ganzzahlen) in das Format, das der Prozessor nativ verarbeiten kann. Wenn wir das Ergebnis wieder in einem 31-Bit-Wert speichern möchten, gibt es ein oder zwei weitere Anweisungen, um dies zu tun.
Diese zusätzliche Komplexität bedeutet, dass Sie sich nur dann darum kümmern würden, wenn die Platzersparnis sehr wichtig ist. In diesem Fall sparen wir nur zwei Bits im Vergleich zur Verwendung des nativen unsigned
oder uint32_t
Typs, der viel einfacheren Code generiert hätte.
Ein Hinweis zu dynamischen Größen:
Das obige Beispiel enthält weiterhin Werte mit fester Breite und keine Werte mit variabler Breite, aber die Breite (und Ausrichtung) stimmen nicht mehr mit den nativen Registern überein.
Die x86-Plattform verfügt über mehrere native Größen, einschließlich 8-Bit und 16-Bit zusätzlich zum 32-Bit-Hauptmodus (der Einfachheit halber beschönige ich den 64-Bit-Modus und verschiedene andere Dinge).
Diese Typen (char, int8_t, uint8_t, int16_t usw.) werden auch direkt von der Architektur unterstützt - teilweise aus Gründen der Abwärtskompatibilität mit älteren 8086/286/386 / etc. usw. Befehlssätze.
Es ist sicherlich der Fall, dass die Auswahl des kleinsten natürlichen Typs mit fester Größe , der ausreicht, eine gute Praxis sein kann - sie sind immer noch schnell, einzelne Anweisungen werden geladen und gespeichert, Sie erhalten immer noch native Arithmetik mit voller Geschwindigkeit und Sie können sogar die Leistung verbessern, indem Sie Reduzieren von Cache-Fehlern.
Dies unterscheidet sich stark von der Codierung mit variabler Länge. Ich habe mit einigen davon gearbeitet, und sie sind schrecklich. Jede Last wird zu einer Schleife anstelle eines einzelnen Befehls. Jedes Geschäft ist auch eine Schleife. Jede Struktur hat eine variable Länge, daher können Sie Arrays nicht auf natürliche Weise verwenden.
Ein weiterer Hinweis zur Effizienz
In den folgenden Kommentaren haben Sie das Wort "effizient" verwendet, soweit ich dies in Bezug auf die Speichergröße beurteilen kann. Manchmal minimieren wir die Speichergröße. Dies kann wichtig sein, wenn wir eine sehr große Anzahl von Werten in Dateien speichern oder über ein Netzwerk senden. Der Nachteil ist, dass wir diese Werte in Register laden müssen, um etwas damit zu tun , und die Durchführung der Konvertierung nicht kostenlos ist.
Wenn wir über Effizienz sprechen, müssen wir wissen, was wir optimieren und welche Kompromisse es gibt. Die Verwendung nicht nativer Speichertypen ist eine Möglichkeit, die Verarbeitungsgeschwindigkeit gegen Speicherplatz zu tauschen, und ist manchmal sinnvoll. Durch die Verwendung von Speicher variabler Länge (zumindest für arithmetische Typen) wird eine höhere Verarbeitungsgeschwindigkeit (und Codekomplexität sowie Entwicklerzeit) gegen eine häufig minimale weitere Platzersparnis eingetauscht.
Die Geschwindigkeitsstrafe, die Sie dafür zahlen, bedeutet, dass es sich nur lohnt, wenn Sie die Bandbreite oder den Langzeitspeicher absolut minimieren müssen. In diesen Fällen ist es normalerweise einfacher, ein einfaches und natürliches Format zu verwenden - und es dann einfach mit einem Allzwecksystem zu komprimieren (wie zip, gzip, bzip2, xy oder was auch immer).
tl; dr
Jede Plattform hat eine Architektur, aber Sie können eine im Wesentlichen unbegrenzte Anzahl verschiedener Arten der Darstellung von Daten finden. Es ist für keine Sprache sinnvoll, eine unbegrenzte Anzahl integrierter Datentypen bereitzustellen. Daher bietet C ++ impliziten Zugriff auf die nativen, natürlichen Datentypen der Plattform und ermöglicht es Ihnen, jede andere (nicht native) Darstellung selbst zu codieren.
unsinged
Wert, der mit 1 Byte dargestellt werden kann, ist255
. 2) Berücksichtigen Sie den Aufwand für die Berechnung der optimalen Speichergröße und das Verkleinern / Erweitern des Speicherbereichs einer Variablen, wenn sich der Wert ändert.