Warum haben Typen immer eine bestimmte Größe, unabhängig von ihrem Wert?

149

Implementierungen können sich zwischen den tatsächlichen Größen der Typen unterscheiden, aber in den meisten Fällen sind Typen wie unsigned int und float immer 4 Byte. Aber warum belegt ein Typ unabhängig von seinem Wert immer eine bestimmte Menge an Speicher? Zum Beispiel, wenn ich die folgende Ganzzahl mit dem Wert 255 erstellt habe

int myInt = 255;

Dann myIntwürde ich mit meinem Compiler 4 Bytes belegen. Der tatsächliche Wert 255kann jedoch mit nur 1 Byte dargestellt werden. Warum sollte also myIntnicht einfach 1 Byte Speicher belegt werden? Oder die allgemeinere Art zu fragen: Warum ist einem Typ nur eine Größe zugeordnet, wenn der zur Darstellung des Werts erforderliche Speicherplatz möglicherweise kleiner als diese Größe ist?

Nichlas Uden
quelle
15
1) " Der tatsächliche Wert 256 kann jedoch mit nur 1 Byte dargestellt werden. " Falsch, der größte unsingedWert, der mit 1 Byte dargestellt werden kann, ist 255. 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.
Algirdas Preidžius
99
Nun, wenn die Zeit gekommen ist, den Wert aus dem Speicher zu lesen , wie schlägt die Maschine vor, zu bestimmen, wie viele Bytes gelesen werden sollen? Woher weiß die Maschine, wo sie den Wert nicht mehr lesen kann? Dies erfordert zusätzliche Einrichtungen. Und im Allgemeinen ist der Speicher- und Leistungsaufwand für diese zusätzlichen Einrichtungen viel höher als im Fall der einfachen Verwendung fester 4 Bytes als unsigned intWert.
Am
74
Diese Frage gefällt mir sehr gut. Auch wenn es einfach zu beantworten scheint, denke ich, dass eine genaue Erklärung ein gutes Verständnis der tatsächlichen Funktionsweise von Computern und Computerarchitekturen erfordert. Die meisten Leute werden es wahrscheinlich für selbstverständlich halten, ohne eine umfassende Erklärung dafür zu haben.
Andreee
37
Überlegen Sie, was passieren würde, wenn Sie dem Wert der Variablen 1 hinzufügen und ihn auf 256 setzen, sodass sie erweitert werden müsste. Wohin expandiert es? Verschieben Sie den Rest des Speichers, um Platz zu schaffen? Bewegt sich die Variable selbst? Wenn ja, wohin bewegt es sich und wie finden Sie die Zeiger, die Sie aktualisieren müssen?
Molbdnilo
13
@someidiot nein, du liegst falsch. std::vector<X>hat immer die gleiche Größe, dh sizeof(std::vector<X>)ist eine Konstante zur Kompilierungszeit.
SergeyA

Antworten:

131

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 intVariablen 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 -O3Einfachheit halber) zu:

square(int):
  imul edi, edi
  mov eax, edi
  ret

das heisst:

  1. Der int numParameter 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
  2. Die Multiplikation ist eine einzelne Anweisung ( imul), die sehr schnell ist
  3. 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 unsignedoder uint32_tTyps, 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.

Nutzlos
quelle
Ich schaue mir all die netten Antworten an, während ich versuche, sie alle zu verstehen. In Bezug auf Ihre Antwort würde eine dynamische Größe, sagen wir weniger als 32 Bit für eine Ganzzahl, nicht nur mehr Variablen in einem Register zulassen ? Wenn die Endianess dieselbe ist, warum wäre dies nicht optimal?
Nichlas Uden
7
@asd aber wie viele Register werden Sie in dem Code verwenden, der herausfindet, wie viele Variablen derzeit in einem Register gespeichert sind?
user253751
1
FWIW ist es üblich, mehrere Werte auf den kleinsten verfügbaren Platz zu packen, wo Sie entscheiden, dass die Platzersparnis wichtiger ist als die Geschwindigkeitskosten für das Ein- und Auspacken. Sie können sie im Allgemeinen nicht auf natürliche Weise in ihrer gepackten Form bearbeiten, da der Prozessor nicht weiß, wie er mit etwas anderem als seinen eingebauten Registern richtig rechnen kann. Suchen Sie nach BCD für eine teilweise Ausnahme mit Prozessorunterstützung
Nutzlos
3
Wenn ich tatsächlich tun alle 32 Bits für einen Wert brauche, ich brauche noch irgendwo die Länge zu speichern, so dass ich jetzt brauchen mehr als 32 Bits in einigen Fällen.
Useless
1
+1. Ein Hinweis, dass "einfaches und natürliches Formatieren und dann komprimieren" normalerweise besser ist: Dies ist definitiv allgemein richtig , aber : Für einige Daten ist VLQ-jeder-Wert-dann-komprimieren-das-Ganze-Ding deutlich besser als nur das Komprimieren-das - Ganzes, und für einige Anwendungen können Ihre Daten nicht zusammen komprimiert werden , da sie entweder unterschiedlich sind (wie in gitden Metadaten) oder Sie sie tatsächlich im Speicher behalten und gelegentlich zufällig auf einige, aber nicht die meisten zugreifen oder diese ändern müssen die Werte (wie in HTML + CSS-Rendering-Engines) und können daher nur mit etwas wie VLQ an Ort und Stelle gekürzt werden.
mtraceur
139

Da Typen im Wesentlichen Speicher darstellen und als Maximalwert definiert sind, den sie halten können, nicht als aktueller Wert.

Die sehr einfache Analogie wäre ein Haus - ein Haus hat eine feste Größe, unabhängig davon, wie viele Menschen darin leben, und es gibt auch eine Bauordnung, die die maximale Anzahl von Menschen festlegt, die in einem Haus einer bestimmten Größe leben können.

Selbst wenn eine einzelne Person in einem Haus mit 10 Plätzen lebt, wird die Größe des Hauses nicht durch die aktuelle Anzahl der Bewohner beeinflusst.

SergeyA
quelle
31
Ich mag die Analogie. Wenn wir es ein wenig erweitern, könnten wir uns vorstellen, eine Programmiersprache zu verwenden, die keine festen Speichergrößen für Typen verwendet. Dies wäre vergleichbar damit, Räume in unserem Haus abzureißen, wenn sie nicht verwendet werden, und sie bei Bedarf neu aufzubauen (dh Tonnen von Overhead, wenn wir nur ein paar Häuser bauen und sie für die Zeit, in der wir sie brauchen, stehen lassen könnten).
ahouse101
5
"Weil Typen grundsätzlich Speicher darstellen" gilt dies nicht für alle Sprachen (wie z. B. Typoskript)
corvus_192
56
@ corvus_192 Tags haben Bedeutung. Diese Frage ist mit C ++ markiert, nicht mit 'Typoskript'
SergeyA
4
@ ahouse101 In der Tat gibt es eine Reihe von Sprachen mit Ganzzahlen mit unbegrenzter Genauigkeit, die nach Bedarf wachsen. Für diese Sprachen müssen Sie keinen festen Speicher für Variablen zuweisen, sondern sie werden intern als Objektreferenzen implementiert. Beispiele: Lisp, Python.
Barmar
2
@jamesqf Es ist wahrscheinlich kein Zufall, dass die MP-Arithmetik erstmals in Lisp übernommen wurde, das auch die automatische Speicherverwaltung durchführte. Die Designer waren der Ansicht, dass die Auswirkungen auf die Leistung der einfachen Programmierung untergeordnet waren. Und Optimierungstechniken wurden entwickelt, um die Auswirkungen zu minimieren.
Barmar
44

Es ist eine Optimierung und Vereinfachung.

Sie können entweder Objekte mit fester Größe haben. So speichern Sie den Wert.
Oder Sie können Objekte mit variabler Größe haben. Aber Wert und Größe speichern.

Objekte mit fester Größe

Der Code, der die Zahl manipuliert, muss sich nicht um die Größe kümmern. Sie gehen davon aus, dass Sie immer 4 Bytes verwenden und den Code sehr einfach gestalten.

Objekte mit dynamischer Größe

Der Code, den die manipulierte Zahl beim Lesen einer Variablen verstehen muss, muss den Wert und die Größe lesen. Verwenden Sie die Größe, um sicherzustellen, dass alle hohen Bits im Register Null sind.

Wenn Sie den Wert wieder im Speicher ablegen, wenn der Wert seine aktuelle Größe nicht überschritten hat, legen Sie ihn einfach wieder im Speicher ab. Wenn der Wert jedoch verkleinert oder vergrößert wurde, müssen Sie den Speicherort des Objekts an einen anderen Speicherort verschieben, um sicherzustellen, dass es nicht überläuft. Jetzt müssen Sie die Position dieser Zahl verfolgen (da sie sich bewegen kann, wenn sie für ihre Größe zu groß wird). Sie müssen auch alle nicht verwendeten variablen Speicherorte verfolgen, damit sie möglicherweise wiederverwendet werden können.

Zusammenfassung

Der für Objekte mit fester Größe generierte Code ist viel einfacher.

Hinweis

Bei der Komprimierung wird die Tatsache verwendet, dass 255 in ein Byte passt. Es gibt Komprimierungsschemata zum Speichern großer Datenmengen, bei denen unterschiedliche Größenwerte für unterschiedliche Zahlen aktiv verwendet werden. Da es sich jedoch nicht um Live-Daten handelt, haben Sie nicht die oben beschriebenen Komplexitäten. Sie benötigen weniger Speicherplatz zum Speichern der Daten auf Kosten der Komprimierung / Dekomprimierung der Daten zur Speicherung.

Martin York
quelle
4
Dies ist die beste Antwort für mich: Wie verfolgen Sie die Größe? Mit mehr Speicher?
Online Thomas
@ThomasMoors Ja genau: mit mehr Speicher. Wenn Sie z. B. ein dynamisches Array haben, intspeichern einige die Anzahl der Elemente in diesem Array. Das intselbst wird wieder eine feste Größe haben.
Alfe
1
@ThomasMoors Es gibt zwei häufig verwendete Optionen, die beide zusätzlichen Speicher benötigen - entweder haben Sie ein Feld (feste Größe), das angibt, wie viele Daten vorhanden sind (z. B. ein Int für die Arraygröße, oder Zeichenfolgen im Pascal-Stil, bei denen die erste Zeichenfolge vorhanden ist) Element enthält, wie viele Zeichen es gibt), oder Sie können alternativ eine Kette (oder eine komplexere Struktur) haben, in der jedes Element irgendwie feststellt, ob es das letzte ist - z. B. nullterminierte Zeichenfolgen oder die meisten Formen verknüpfter Listen.
Peteris
27

Denn in einer Sprache wie C ++ besteht ein Entwurfsziel darin, dass einfache Operationen zu einfachen Maschinenanweisungen kompiliert werden.

Alle gängigen CPU-Befehlssätze arbeiten mit Typen mit fester Breite. Wenn Sie Typen mit variabler Breite ausführen möchten, müssen Sie mehrere Maschinenbefehle ausführen, um diese zu verarbeiten.

Was , warum die zugrundeliegende Computerhardware ist auf diese Weise: Es ist , weil es einfacher und effizienter für viele Fälle (aber nicht alle).

Stellen Sie sich den Computer als ein Stück Klebeband vor:

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

Wenn Sie den Computer einfach anweisen, das erste Byte auf dem Band zu betrachten, xxwoher weiß er dann, ob der Typ dort stoppt oder mit dem nächsten Byte fortfährt? Wenn Sie eine Zahl wie 255(hexadezimal FF) oder eine Zahl wie 65535(hexadezimal FFFF) haben, ist das erste Byte immer FF.

Woher weißt du das? Sie müssen zusätzliche Logik hinzufügen und die Bedeutung von mindestens einem Bit- oder Bytewert "überladen", um anzuzeigen, dass der Wert bis zum nächsten Byte fortgesetzt wird. Diese Logik ist niemals "frei", entweder Sie emulieren sie in Software oder Sie fügen der CPU eine Reihe zusätzlicher Transistoren hinzu, um dies zu tun.

Die Arten von Sprachen mit fester Breite wie C und C ++ spiegeln dies wider.

Dies muss nicht so sein, und abstraktere Sprachen, die sich weniger mit der Zuordnung zu maximal effizientem Code befassen, können für numerische Typen Codierungen mit variabler Breite (auch als "Variable Length Quantities" oder VLQ bezeichnet) verwenden.

Weiterführende Literatur: Wenn Sie für „variable Länge Quantität“ suchen Sie einige Beispiele finden können , wo diese Art der Codierung ist tatsächlich effizient und lohnt sich die zusätzliche Logik. In der Regel müssen Sie eine große Anzahl von Werten speichern, die sich möglicherweise innerhalb eines großen Bereichs befinden. Die meisten Werte tendieren jedoch zu einem kleinen Teilbereich.


Beachten Sie, dass , wenn ein Compiler kann beweisen , dass es in einer geringeren Menge an Raum mit Speichern des Wertes wegzukommen , ohne Code zu brechen (zum Beispiel ist es eine Variable nur sichtbar intern innerhalb einer einzelnen Übersetzungseinheit), und deren Optimierung Heuristik legen nahe , dass es‘ Um die Zielhardware effizienter zu gestalten, ist es durchaus zulässig, sie entsprechend zu optimieren und auf kleinerem Raum zu speichern, solange der Rest des Codes "so funktioniert, als ob" er die Standardfunktion erfüllt.

Aber , wenn der Code hat Inter arbeiten mit anderem Code, der separat kompiliert werden kann, müssen Größen konsistent bleiben, oder stellen Sie sicher , dass jedes Stück Code die gleiche Konvention folgt.

Denn wenn es nicht konsistent ist, gibt es diese Komplikation: Was ist, wenn ich es habe, int x = 255;aber später im Code, den ich mache x = y? Wenn intdie Breite variabel sein könnte, müsste der Compiler dies im Voraus wissen, um den maximal benötigten Speicherplatz vorab zuzuweisen. Das ist nicht immer möglich, denn was ist, wenn yein Argument von einem anderen Code übergeben wird, der separat kompiliert wurde?

mtraceur
quelle
26

Java verwendet dazu die Klassen "BigInteger" und "BigDecimal", ebenso wie anscheinend die GMP C ++ - Klassenschnittstelle von C ++ (danke Digital Trauma). Sie können es ganz einfach in so ziemlich jeder Sprache selbst machen, wenn Sie wollen.

CPUs hatten schon immer die Möglichkeit, BCD (Binary Coded Decimal) zu verwenden, das Operationen beliebiger Länge unterstützt (Sie arbeiten jedoch in der Regel manuell mit jeweils einem Byte, was nach den heutigen GPU-Standards langsamer wäre).

Der Grund, warum wir diese oder ähnliche Lösungen nicht verwenden? Performance. Ihre leistungsstärksten Sprachen können es sich nicht leisten, eine Variable mitten in einer Operation mit engen Schleifen zu erweitern - dies wäre sehr nicht deterministisch.

In Massenspeicher- und Transportsituationen sind verpackte Werte häufig die EINZIGE Art von Wert, die Sie verwenden würden. Beispielsweise kann ein Musik- / Videopaket, das auf Ihren Computer gestreamt wird, etwas Zeit in Anspruch nehmen, um anzugeben, ob der nächste Wert als Größenoptimierung 2 Byte oder 4 Byte beträgt.

Sobald es sich auf Ihrem Computer befindet, auf dem es verwendet werden kann, ist der Speicher zwar billig, die Geschwindigkeit und Komplikation von Variablen mit veränderbarer Größe jedoch nicht. Dies ist wirklich der einzige Grund.

Bill K.
quelle
4
Ich bin froh zu sehen, dass jemand BigInteger erwähnt. Es ist nicht so, dass es eine dumme Idee ist, es ist nur so, dass es nur Sinn macht, es für extrem große Zahlen zu tun.
Max Barraclough
1
Um pedantisch zu sein, meinst du eigentlich extrem genaue Zahlen :) Nun, zumindest im Fall von BigDecimal ...
Bill K
2
Und da dies mit c ++ gekennzeichnet ist , ist es wahrscheinlich erwähnenswert, die GMP C ++ - Klassenschnittstelle zu erwähnen , die dieselbe Idee wie Javas Big * ist.
Digitales Trauma
20

Weil es sehr kompliziert und rechenintensiv wäre, einfache Typen mit dynamischen Größen zu haben. Ich bin mir nicht sicher, ob dies überhaupt möglich wäre.
Der Computer müsste prüfen, wie viele Bits die Zahl nach jeder Änderung ihres Wertes benötigt. Es wären ziemlich viele zusätzliche Operationen. Und es wäre viel schwieriger, Berechnungen durchzuführen, wenn Sie die Größe der Variablen während der Kompilierung nicht kennen.

Um dynamische Größen von Variablen zu unterstützen, müsste sich der Computer tatsächlich merken, wie viele Bytes eine Variable gerade hat, was ... zusätzlichen Speicher zum Speichern dieser Informationen erfordern würde. Und diese Informationen müssten vor jeder Operation an der Variablen analysiert werden, um den richtigen Prozessorbefehl auszuwählen.

Um besser zu verstehen, wie Computer funktionieren und warum Variablen konstante Größen haben, lernen Sie die Grundlagen der Assembler-Sprache.

Obwohl ich denke, dass es möglich wäre, so etwas mit constexpr-Werten zu erreichen. Dies würde jedoch den Code für einen Programmierer weniger vorhersehbar machen. Ich nehme an, dass einige Compiler-Optimierungen so etwas tun, aber sie verbergen es vor einem Programmierer, um die Dinge einfach zu halten.

Ich habe hier nur die Probleme beschrieben, die die Leistung eines Programms betreffen. Ich habe alle Probleme weggelassen, die gelöst werden müssten, um Speicherplatz zu sparen, indem ich die Größe der Variablen reduzierte. Ehrlich gesagt denke ich nicht, dass es überhaupt möglich ist.


Zusammenfassend ist die Verwendung kleinerer Variablen als deklariert nur dann sinnvoll, wenn ihre Werte während der Kompilierung bekannt sind. Es ist sehr wahrscheinlich, dass moderne Compiler dies tun. In anderen Fällen würde dies zu viele schwierige oder sogar unlösbare Probleme verursachen.

KEIN NAME
quelle
Ich bezweifle sehr, dass so etwas während der Kompilierungszeit gemacht wird. Es macht wenig Sinn, den Compiler-Speicher so zu erhalten, und das ist der einzige Vorteil.
Bartek Banachewicz
1
Ich dachte eher an Operationen wie das Multiplizieren der constexpr-Variablen mit der normalen Variablen. Zum Beispiel haben wir (theoretisch) eine 8-Byte-Constexpr-Variable mit Wert 56und multiplizieren sie mit einer 2-Byte-Variablen. Bei einigen Architekturen wäre der 64-Bit-Betrieb rechenintensiver, sodass der Compiler dies optimieren könnte, um nur eine 16-Bit-Multiplikation durchzuführen.
NO_NAME
Einige APL-Implementierungen und einige Sprachen in der SNOBOL-Familie (SPITBOL, glaube ich? Vielleicht Icon) haben genau dies getan (mit Granularität): Ändern Sie das Darstellungsformat dynamisch in Abhängigkeit von den tatsächlichen Werten. APL würde von Boolean zu Integer gehen, um zu schweben und zurück. SPITBOL würde von der Spaltendarstellung von Booleschen Werten (8 separate Boolesche Arrays, die in einem Bytearray gespeichert sind) zu Ganzzahlen (IIRC) wechseln.
Davidbak
16

Dann myIntwürde ich mit meinem Compiler 4 Bytes belegen. Der tatsächliche Wert 255kann jedoch mit nur 1 Byte dargestellt werden. Warum sollte also myIntnicht einfach 1 Byte Speicher belegt werden?

Dies ist als Codierung mit variabler Länge bekannt . Es sind verschiedene Codierungen definiert, beispielsweise VLQ . Eines der bekanntesten ist jedoch wahrscheinlich UTF-8 : UTF-8 codiert Codepunkte auf einer variablen Anzahl von Bytes von 1 bis 4.

Oder die allgemeinere Art zu fragen: Warum ist einem Typ nur eine Größe zugeordnet, wenn der zur Darstellung des Werts erforderliche Speicherplatz möglicherweise kleiner als diese Größe ist?

Wie immer in der Technik dreht sich alles um Kompromisse. Es gibt keine Lösung, die nur Vorteile bietet. Sie müssen also bei der Entwicklung Ihrer Lösung Vorteile und Kompromisse in Einklang bringen.

Das Design, für das entschieden wurde, bestand darin, grundlegende Typen mit fester Größe zu verwenden, und die Hardware / Sprachen flogen einfach von dort herunter.

Was ist also die grundlegende Schwäche der variablen Codierung , die dazu führte, dass sie zugunsten speicherhungrigerer Schemata abgelehnt wurde? Keine zufällige Adressierung .

Was ist der Index des Bytes, an dem der 4. Codepunkt in einer UTF-8-Zeichenfolge beginnt?

Dies hängt von den Werten der vorherigen Codepunkte ab. Ein linearer Scan ist erforderlich.

Sicherlich gibt es Codierungsschemata mit variabler Länge, die sich besser für die zufällige Adressierung eignen.

Ja, aber sie sind auch komplizierter. Wenn es ein ideales gibt, habe ich es noch nie gesehen.

Ist zufällige Adressierung überhaupt wichtig?

Oh ja!

Die Sache ist, dass jede Art von Aggregat / Array auf Typen mit fester Größe beruht:

  • Zugriff auf das 3. Feld eines struct? Zufällige Adressierung!
  • Zugriff auf das 3. Element eines Arrays? Zufällige Adressierung!

Was bedeutet, dass Sie im Wesentlichen den folgenden Kompromiss haben:

Feste Größentypen ODER lineare Speicherscans

Matthieu M.
quelle
Dies ist kein so großes Problem, wie Sie es klingen lassen. Sie können immer Vektortabellen verwenden. Es gibt einen Speicheraufwand und einen zusätzlichen Abruf, aber lineare Scans sind nicht erforderlich.
Artelius
2
@Artelius: Wie codiert man die Vektortabelle, wenn Ganzzahlen eine variable Breite haben? Wie hoch ist der Speicheraufwand der Vektortabelle beim Codieren einer für Ganzzahlen, die 1 bis 4 Byte im Speicher verwenden?
Matthieu M.
Schauen Sie, Sie haben Recht, in dem spezifischen Beispiel, das das OP gegeben hat, hat die Verwendung von Vektortabellen keinen Vorteil. Anstatt eine Vektortabelle zu erstellen, können Sie die Daten auch in ein Array von Elementen fester Größe einfügen. Das OP forderte jedoch auch eine allgemeinere Antwort. In Python, ein Array von ganzen Zahlen ist ein Vektortabelle von variabler Größe Zahlen! Das liegt nicht daran, dass dieses Problem gelöst wird, sondern daran, dass Python beim Kompilieren nicht weiß, ob es sich bei den Listenelementen um Ganzzahlen, Gleitkommazahlen, Dikte, Zeichenfolgen oder Listen handelt, die natürlich alle unterschiedliche Größen haben.
Artelius
@Artelius: Beachten Sie, dass das Array in Python Zeiger fester Größe auf Elemente enthält. Dies macht es O (1), auf Kosten einer Indirektion zu einem Element zu gelangen.
Matthieu M.
16

Der Computerspeicher ist in nacheinander adressierte Blöcke einer bestimmten Größe (häufig 8 Bit und als Bytes bezeichnet) unterteilt, und die meisten Computer sind so konzipiert, dass sie effizient auf Folgen von Bytes mit aufeinanderfolgenden Adressen zugreifen können.

Wenn sich die Adresse eines Objekts innerhalb der Lebensdauer des Objekts nie ändert, kann der mit seiner Adresse angegebene Code schnell auf das betreffende Objekt zugreifen. Eine wesentliche Einschränkung bei diesem Ansatz besteht jedoch darin, dass X innerhalb der Lebensdauer nicht größer als N Bytes werden kann, wenn eine Adresse für die Adresse X zugewiesen wird und dann eine andere Adresse für die Adresse Y zugewiesen wird, die N Bytes entfernt ist von Y, es sei denn, entweder X oder Y wird bewegt. Damit sich X bewegen kann, muss alles im Universum, das die Adresse von X enthält, aktualisiert werden, um die neue Adresse wiederzugeben, und Y muss sich ebenfalls bewegen. Während es möglich ist, ein System zu entwerfen, das solche Updates erleichtert (sowohl Java als auch .NET verwalten es ziemlich gut), ist es viel effizienter, mit Objekten zu arbeiten, die während ihrer gesamten Lebensdauer am selben Ort bleiben.

Superkatze
quelle
"X kann innerhalb der Lebensdauer von Y nicht größer als N Bytes werden, es sei denn, X oder Y werden verschoben. Damit X verschoben werden kann, muss alles im Universum, das die Adresse von X enthält, aktualisiert werden der neue, und ebenso für Y, um sich zu bewegen. " Dies ist der herausragende Punkt IMO: Objekte, die nur so viel Größe verwenden, wie ihr aktueller Wert benötigt, müssten Tonnen von Overhead für Größen / Sentinels, Speicherverschiebungen, Referenzdiagramme usw. hinzufügen . Und ganz offensichtlich, wenn man darüber nachdenkt, wie es jemals funktionieren könnte ... aber dennoch sehr erwähnenswert, besonders wie so wenige andere.
underscore_d
@underscore_d: Sprachen wie Javascript, die von Grund auf für den Umgang mit Objekten variabler Größe entwickelt wurden, können dabei erstaunlich effizient sein. Auf der anderen Seite ist es zwar möglich, Objektsysteme mit variabler Größe einfach und schnell zu gestalten, die einfachen Implementierungen sind jedoch langsam und die schnellen Implementierungen äußerst komplex.
Supercat
13

Die kurze Antwort lautet: Weil der C ++ - Standard dies sagt.

Die lange Antwort lautet: Was Sie auf einem Computer tun können, ist letztendlich durch die Hardware begrenzt. Es ist natürlich möglich, eine Ganzzahl in eine variable Anzahl von Bytes für die Speicherung zu codieren, aber dann würde das Lesen entweder spezielle CPU-Anweisungen erfordern, um performant zu sein, oder Sie könnten sie in Software implementieren, aber dann wäre es furchtbar langsam. In der CPU stehen Operationen mit fester Größe zum Laden von Werten vordefinierter Breiten zur Verfügung. Für variable Breiten gibt es keine.

Ein weiterer zu berücksichtigender Punkt ist die Funktionsweise des Computerspeichers. Angenommen, Ihr Integer-Typ kann zwischen 1 und 4 Byte Speicherplatz beanspruchen. Angenommen, Sie speichern den Wert 42 in Ihrer Ganzzahl: Er nimmt 1 Byte ein und platziert ihn an der Speicheradresse X. Dann speichern Sie Ihre nächste Variable an Position X + 1 (ich erwäge an dieser Stelle keine Ausrichtung) und so weiter . Später entscheiden Sie sich, Ihren Wert in 6424 zu ändern.

Dies passt aber nicht in ein einzelnes Byte! Also, was machst du? Wo legst du den Rest hin? Sie haben bereits etwas bei X + 1, können es also nicht dort platzieren. Irgendwo anders? Woher wissen Sie später, wo? Der Computerspeicher unterstützt keine Einfügesemantik: Sie können nicht einfach etwas an einem Ort platzieren und alles danach beiseite schieben, um Platz zu schaffen!

Nebenbei: Sie sprechen wirklich vom Bereich der Datenkomprimierung. Es gibt Komprimierungsalgorithmen, mit denen alles enger gepackt werden kann. Zumindest einige von ihnen werden in Betracht ziehen, nicht mehr Speicherplatz für Ihre Ganzzahl zu verwenden, als sie benötigt. Komprimierte Daten sind jedoch nicht einfach zu ändern (wenn überhaupt möglich) und werden jedes Mal neu komprimiert, wenn Sie Änderungen daran vornehmen.

John Doe der Gerechte
quelle
11

Dies bietet erhebliche Vorteile für die Laufzeitleistung. Wenn Sie mit Typen mit variabler Größe arbeiten möchten, müssen Sie jede Zahl vor der Operation dekodieren (Maschinencode-Anweisungen haben normalerweise eine feste Breite), die Operation ausführen und dann einen Speicherplatz im Speicher finden, der groß genug ist, um das Ergebnis aufzunehmen. Das sind sehr schwierige Operationen. Es ist viel einfacher, alle Daten einfach ineffizient zu speichern.

So wird es nicht immer gemacht. Betrachten Sie das Protobuf-Protokoll von Google. Protobufs sind so konzipiert, dass sie Daten sehr effizient übertragen. Das Verringern der Anzahl der übertragenen Bytes ist die Kosten für zusätzliche Anweisungen beim Bearbeiten der Daten wert. Dementsprechend verwenden Protobufs eine Codierung, die Ganzzahlen in 1, 2, 3, 4 oder 5 Bytes codiert, und kleinere Ganzzahlen benötigen weniger Bytes. Sobald die Nachricht empfangen wurde, wird sie jedoch in ein herkömmlicheres Ganzzahlformat mit fester Größe entpackt, das einfacher zu bearbeiten ist. Nur während der Netzwerkübertragung verwenden sie eine so platzsparende Ganzzahl variabler Länge.

Cort Ammon
quelle
11

Ich mag Sergeys Hausanalogie , aber ich denke, eine Autoanalogie wäre besser.

Stellen Sie sich Variablentypen als Autotypen und Personen als Daten vor. Wenn wir nach einem neuen Auto suchen, wählen wir das, das am besten zu unserem Zweck passt. Wollen wir ein kleines intelligentes Auto, das nur für ein oder zwei Personen geeignet ist? Oder eine Limousine, um mehr Menschen zu befördern? Beide haben ihre Vor- und Nachteile wie Geschwindigkeit und Kraftstoffverbrauch (denken Sie an Geschwindigkeit und Speichernutzung).

Wenn Sie eine Limousine haben und alleine fahren, wird sie nicht schrumpfen, um nur Ihnen zu passen. Dazu müssten Sie das Auto verkaufen (sprich: freigeben) und sich ein neues kleineres kaufen.

Wenn Sie die Analogie fortsetzen, können Sie sich das Gedächtnis als einen riesigen Parkplatz vorstellen, der mit Autos gefüllt ist, und wenn Sie zum Lesen gehen, holt ein spezialisierter Chauffeur, der ausschließlich für Ihren Autotyp ausgebildet ist, es für Sie ab. Wenn Ihr Auto je nach Person den Typ ändern könnte, müssten Sie jedes Mal eine ganze Reihe von Chauffeuren mitbringen, wenn Sie Ihr Auto bekommen möchten, da diese nie wissen würden, welche Art von Auto vor Ort sitzen wird.

Mit anderen Worten, der Versuch, festzustellen, wie viel Speicher Sie zur Laufzeit lesen müssen, wäre äußerst ineffizient und überwiegt die Tatsache, dass Sie möglicherweise noch ein paar Autos auf Ihrem Parkplatz unterbringen könnten.

scohe001
quelle
10

Es gibt einige Gründe. Eine davon ist die zusätzliche Komplexität bei der Verarbeitung von Zahlen beliebiger Größe und die damit verbundene Leistungseinbuße, da der Compiler nicht mehr unter der Annahme optimieren kann, dass jedes int genau X Byte lang ist.

Ein zweiter Grund ist, dass das Speichern einfacher Typen auf diese Weise bedeutet, dass sie ein zusätzliches Byte benötigen, um die Länge zu halten. Ein Wert von 255 oder weniger benötigt in diesem neuen System tatsächlich zwei Bytes, nicht eines, und im schlimmsten Fall benötigen Sie jetzt 5 Bytes anstelle von 4. Dies bedeutet, dass der Leistungsgewinn in Bezug auf den verwendeten Speicher geringer ist als Sie Denken Sie und in einigen Randfällen könnte tatsächlich ein Nettoverlust sein.

Ein dritter Grund ist, dass der Computerspeicher im Allgemeinen in Worten und nicht in Bytes adressierbar ist . (Aber siehe Fußnote). Wörter sind ein Vielfaches von Bytes, normalerweise 4 auf 32-Bit-Systemen und 8 auf 64-Bit-Systemen. Normalerweise können Sie kein einzelnes Byte lesen, Sie lesen ein Wort und extrahieren das n-te Byte aus diesem Wort. Dies bedeutet sowohl, dass das Extrahieren einzelner Bytes aus einem Wort etwas aufwändiger ist als nur das Lesen des gesamten Wortes, als auch, dass es sehr effizient ist, wenn der gesamte Speicher gleichmäßig in wortgroße (dh 4-Byte-große) Blöcke unterteilt ist. Wenn Sie Ganzzahlen beliebiger Größe haben, kann es sein, dass ein Teil der Ganzzahl in einem Wort und ein anderer im nächsten Wort enthalten ist und zwei Lesevorgänge erforderlich sind, um die vollständige Ganzzahl zu erhalten.

Fußnote: Genauer gesagt, während Sie in Bytes angesprochen haben, haben die meisten Systeme die "ungeraden" Bytes ignoriert. Das heißt, Adresse 0, 1, 2 und 3 lesen alle dasselbe Wort, 4, 5, 6 und 7 lesen das nächste Wort und so weiter.

Dies ist auch der Grund, warum 32-Bit-Systeme maximal 4 GB Speicher hatten. Die Register, die zum Adressieren von Speicherorten im Speicher verwendet werden, sind normalerweise groß genug, um ein Wort aufzunehmen, dh 4 Bytes, das einen Maximalwert von (2 ^ 32) -1 = 4294967295 hat. 4294967296 Bytes sind 4 GB.

Buurman
quelle
8

Es gibt Objekte in der C ++ - Standardbibliothek, die in gewissem Sinne eine variable Größe haben, wie z std::vector. Diese weisen jedoch dynamisch den zusätzlichen Speicher zu, den sie benötigen. Wenn Sie nehmen sizeof(std::vector<int>), erhalten Sie eine Konstante, die nichts mit dem vom Objekt verwalteten Speicher zu tun hat. Wenn Sie ein Array oder eine Struktur zuweisen std::vector<int>, die diese enthält , wird diese Basisgröße reserviert, anstatt den zusätzlichen Speicher in dasselbe Array oder dieselbe Struktur zu stellen . Es gibt einige Teile der C-Syntax, die so etwas unterstützen, insbesondere Arrays und Strukturen mit variabler Länge, aber C ++ hat sich nicht dafür entschieden, sie zu unterstützen.

Der Sprachstandard definiert die Objektgröße auf diese Weise, damit Compiler effizienten Code generieren können. Wenn beispielsweise intbei einer Implementierung 4 Byte lang sind und Sie aals Zeiger oder Array von intWerten deklarieren , wird dies a[i]in den Pseudocode übersetzt: "Dereferenzieren Sie die Adresse a + 4 × i." Dies kann in konstanter Zeit erfolgen und ist eine so häufige und wichtige Operation, dass viele Befehlssatzarchitekturen, einschließlich x86 und der DEC-PDP-Maschinen, auf denen C ursprünglich entwickelt wurde, dies in einem einzigen Maschinenbefehl ausführen können.

Ein gängiges Beispiel aus der Praxis für Daten, die nacheinander als Einheiten variabler Länge gespeichert werden, sind Zeichenfolgen, die als UTF-8 codiert sind. (Der zugrunde liegende Typ einer UTF-8-Zeichenfolge für den Compiler ist jedoch weiterhin charund hat die Breite 1. Dadurch können ASCII-Zeichenfolgen als gültige UTF-8-Zeichenfolge interpretiert werden und viele Bibliothekscodes wie strlen()und können strncpy()weiterhin verwendet werden.) Die Codierung eines UTF-8-Codepunkts kann ein bis vier Byte lang sein. Wenn Sie also den fünften UTF-8-Codepunkt in einer Zeichenfolge verwenden möchten, kann er vom fünften bis zum siebzehnten Byte der Daten beginnen. Die einzige Möglichkeit, dies zu finden, besteht darin, vom Anfang der Zeichenfolge aus zu scannen und die Größe jedes Codepunkts zu überprüfen. Wenn Sie das fünfte Graphem finden möchtenmüssen Sie auch die Zeichenklassen überprüfen. Wenn Sie das millionste UTF-8-Zeichen in einer Zeichenfolge finden möchten, müssen Sie diese Schleife millionenfach ausführen! Wenn Sie wissen, dass Sie häufig mit Indizes arbeiten müssen, können Sie die Zeichenfolge einmal durchlaufen und einen Index daraus erstellen - oder Sie können in eine Codierung mit fester Breite wie UCS-4 konvertieren. Um das millionste UCS-4-Zeichen in einer Zeichenfolge zu finden, müssen nur vier Millionen zur Adresse des Arrays hinzugefügt werden.

Eine weitere Komplikation bei Daten variabler Länge besteht darin, dass Sie beim Zuweisen entweder so viel Speicher zuweisen müssen, wie jemals verwendet werden könnte, oder bei Bedarf dynamisch neu zuweisen müssen. Die Zuweisung für den schlimmsten Fall kann äußerst verschwenderisch sein. Wenn Sie einen aufeinanderfolgenden Speicherblock benötigen, kann die Neuzuweisung dazu führen, dass Sie alle Daten an einen anderen Speicherort kopieren müssen. Die Speicherung des Speichers in nicht aufeinanderfolgenden Blöcken erschwert jedoch die Programmlogik.

So ist es möglich , mit variabler Länge bignums statt fester Breite haben short int, int, long intund long long int, aber es wäre ineffizient sein , um sie zuzuweisen und zu verwenden. Darüber hinaus sind alle Mainstream-CPUs für die Arithmetik in Registern mit fester Breite ausgelegt, und keine enthält Anweisungen, die direkt mit einer Art Bignum variabler Länge arbeiten. Diese müssten viel langsamer in Software implementiert werden.

In der realen Welt haben die meisten (aber nicht alle) Programmierer entschieden, dass die Vorteile der UTF-8-Codierung, insbesondere die Kompatibilität, wichtig sind und dass wir uns so selten um etwas anderes kümmern, als einen String von vorne nach hinten zu scannen oder Blöcke von zu kopieren Speicher, dass die Nachteile der variablen Breite akzeptabel sind. Wir könnten gepackte Elemente mit variabler Breite ähnlich wie UTF-8 für andere Dinge verwenden. Aber wir tun es sehr selten und sie sind nicht in der Standardbibliothek.

Davislor
quelle
7

Warum ist einem Typ nur eine Größe zugeordnet, wenn der zur Darstellung des Werts erforderliche Speicherplatz möglicherweise kleiner als diese Größe ist?

In erster Linie aufgrund von Ausrichtungsanforderungen.

Wie pro basic.align / 1 :

Objekttypen haben Ausrichtungsanforderungen, die die Adressen einschränken, an denen ein Objekt dieses Typs zugewiesen werden kann.

Stellen Sie sich ein Gebäude mit vielen Etagen und jeder Etage mit vielen Räumen vor.
Jeder Raum hat Ihre Größe (ein fester Raum) und kann N Personen oder Gegenstände aufnehmen.
Mit der vorher bekannten Raumgröße ist die strukturelle Komponente des Gebäudes gut strukturiert .

Wenn die Räume nicht ausgerichtet sind, ist das Gebäudeskelett nicht gut strukturiert.

Joseph D.
quelle
7

Es kann weniger sein. Betrachten Sie die Funktion:

int foo()
{
    int bar = 1;
    int baz = 42;
    return bar+baz;
}

es wird zu Assembler-Code kompiliert (g ++, x64, Details entfernt)

$43, %eax
ret

Hier barund am bazEnde verwenden Sie null Bytes zur Darstellung.

max630
quelle
5

Warum sollte myInt nicht nur 1 Byte Speicher belegen?

Weil du es so oft benutzt hast. Bei Verwendung von a unsigned intschreiben einige Standards vor, dass 4 Bytes verwendet werden und dass der verfügbare Bereich dafür zwischen 0 und 4.294.967.295 liegt. Wenn Sie unsigned charstattdessen ein verwenden würden, würden Sie wahrscheinlich nur das gesuchte 1-Byte verwenden (abhängig vom Standard und C ++ verwendet normalerweise diese Standards).

Ohne diese Standards müssten Sie Folgendes berücksichtigen: Woher soll der Compiler oder die CPU wissen, dass nur 1 Byte anstelle von 4 verwendet wird? Später in Ihrem Programm können Sie diesen Wert addieren oder multiplizieren, was mehr Speicherplatz erfordern würde. Wann immer Sie eine Speicherzuweisung vornehmen, muss das Betriebssystem diesen Speicherplatz finden, zuordnen und Ihnen zur Verfügung stellen (möglicherweise wird auch Speicher in den virtuellen Arbeitsspeicher verschoben). Dies kann lange dauern. Wenn Sie den Speicher vorher zuweisen, müssen Sie nicht warten, bis eine weitere Zuordnung abgeschlossen ist.

Was den Grund betrifft, warum wir 8 Bits pro Byte verwenden, können Sie sich Folgendes ansehen: Wie ist die Geschichte, warum Bytes acht Bits sind?

Nebenbei bemerkt, Sie könnten zulassen, dass die Ganzzahl überläuft. Sollten Sie jedoch eine vorzeichenbehaftete Ganzzahl verwenden, geben die C \ C ++ - Standards an, dass Ganzzahlüberläufe zu undefiniertem Verhalten führen. Ganzzahliger Überlauf

Blerg
quelle
5

Etwas Einfaches, das die meisten Antworten zu vermissen scheinen:

weil es den Designzielen von C ++ entspricht.

Die Möglichkeit, die Größe eines Typs zur Kompilierungszeit zu ermitteln, ermöglicht es dem Compiler und dem Programmierer, eine Vielzahl von vereinfachenden Annahmen zu treffen, die insbesondere in Bezug auf die Leistung viele Vorteile bringen. Natürlich haben Typen mit fester Größe gleichzeitig Fallstricke wie einen ganzzahligen Überlauf. Aus diesem Grund treffen verschiedene Sprachen unterschiedliche Entwurfsentscheidungen. (Zum Beispiel haben Python-Ganzzahlen im Wesentlichen eine variable Größe.)

Wahrscheinlich ist der Hauptgrund, warum sich C ++ so stark an Typen mit fester Größe orientiert, das Ziel der C-Kompatibilität. Da C ++ jedoch eine statisch typisierte Sprache ist, die versucht, sehr effizienten Code zu generieren und das Hinzufügen von Dingen vermeidet, die vom Programmierer nicht explizit angegeben wurden, sind Typen mit fester Größe immer noch sehr sinnvoll.

Warum hat sich C überhaupt für Typen mit fester Größe entschieden? Einfach. Es wurde entwickelt, um Betriebssysteme, Serversoftware und Dienstprogramme aus den 70er Jahren zu schreiben. Dinge, die Infrastruktur (wie Speicherverwaltung) für andere Software bereitstellten. Auf einem so niedrigen Niveau ist die Leistung entscheidend, und der Compiler tut genau das, was Sie ihm sagen.

Artelius
quelle
5

Das Ändern der Größe einer Variablen würde eine Neuzuweisung erfordern, und dies ist normalerweise die zusätzlichen CPU-Zyklen nicht wert, verglichen mit der Verschwendung einiger weiterer Bytes Speicher.

Lokale Variablen befinden sich auf einem Stapel, der sehr schnell bearbeitet werden kann, wenn sich die Größe dieser Variablen nicht ändert. Wenn Sie beschlossen haben, die Größe einer Variablen von 1 Byte auf 2 Byte zu erweitern, müssen Sie alles auf dem Stapel um ein Byte verschieben, um diesen Platz dafür zu schaffen. Dies kann möglicherweise viele CPU-Zyklen kosten, je nachdem, wie viele Dinge verschoben werden müssen.

Eine andere Möglichkeit besteht darin, jede Variable zu einem Zeiger auf einen Heap-Speicherort zu machen. Auf diese Weise würden Sie jedoch noch mehr CPU-Zyklen und Speicher verschwenden. Zeiger sind 4 Bytes (32-Bit-Adressierung) oder 8 Bytes (64-Bit-Adressierung). Sie verwenden also bereits 4 oder 8 für den Zeiger und dann die tatsächliche Größe der Daten auf dem Heap. In diesem Fall fallen immer noch Kosten für die Neuzuweisung an. Wenn Sie Heap-Daten neu zuweisen müssen, haben Sie möglicherweise Glück und können sie inline erweitern. Manchmal müssen Sie sie jedoch an eine andere Stelle auf dem Heap verschieben, um den zusammenhängenden Speicherblock der gewünschten Größe zu erhalten.

Es ist immer schneller, vorher zu entscheiden, wie viel Speicher verwendet werden soll. Wenn Sie eine dynamische Dimensionierung vermeiden können, gewinnen Sie an Leistung. Die Verschwendung von Speicher ist normalerweise den Leistungsgewinn wert. Deshalb haben Computer jede Menge Speicher. :) :)

Chris Rollins
quelle
3

Der Compiler darf viele Änderungen an Ihrem Code vornehmen, solange die Dinge noch funktionieren (die "wie besehen" -Regel).

Es wäre möglich, einen 8-Bit-Literal-Verschiebungsbefehl anstelle des längeren (32/64 Bit) zu verwenden, der zum Verschieben eines vollständigen Befehls erforderlich ist int. Sie würden jedoch zwei Anweisungen benötigen, um das Laden abzuschließen, da Sie das Register zuerst auf Null setzen müssten, bevor Sie das Laden durchführen.

Es ist einfach effizienter (zumindest laut den Hauptcompilern), den Wert als 32-Bit zu behandeln. Eigentlich habe ich noch keinen x86 / x86_64-Compiler gesehen, der 8-Bit-Ladevorgänge ohne Inline-Assembly ausführen würde.

Bei 64-Bit sieht es jedoch anders aus. Beim Entwerfen der vorherigen Erweiterungen (von 16 auf 32 Bit) ihrer Prozessoren hat Intel einen Fehler gemacht. Hier ist eine gute Darstellung, wie sie aussehen. Das Wichtigste dabei ist, dass wenn Sie an AL oder AH schreiben, der andere nicht betroffen ist (fair genug, das war der Punkt und es machte damals Sinn). Aber es wird interessant, wenn sie es auf 32 Bit erweitert haben. Wenn Sie die unteren Bits (AL, AH oder AX) schreiben, passiert nichts mit den oberen 16 Bits von EAX. Wenn Sie also a charin a umwandeln möchten int, müssen Sie diesen Speicher zuerst löschen, haben aber keine Möglichkeit dazu Tatsächlich werden nur diese Top-16-Bits verwendet, was dieses "Feature" mehr als alles andere zum Schmerz macht.

Mit 64 Bit hat AMD einen viel besseren Job gemacht. Wenn Sie etwas in den unteren 32 Bits berühren, werden die oberen 32 Bits einfach auf 0 gesetzt. Dies führt zu einigen tatsächlichen Optimierungen, die Sie in diesem Godbolt sehen können . Sie können sehen, dass das Laden von 8 Bit oder 32 Bit auf die gleiche Weise erfolgt. Wenn Sie jedoch 64-Bit-Variablen verwenden, verwendet der Compiler abhängig von der tatsächlichen Größe Ihres Literal einen anderen Befehl.

Wie Sie hier sehen können, können Compiler die tatsächliche Größe Ihrer Variablen in der CPU vollständig ändern, wenn sie das gleiche Ergebnis erzielen würden. Für kleinere Typen ist dies jedoch nicht sinnvoll.

Meneldal
quelle
Korrektur: als ob . Ich sehe auch nicht ein, wie, wenn ein kürzeres Laden / Speichern verwendet werden könnte, dies die anderen Bytes für die Verwendung freisetzen würde - was das OP zu fragen scheint: nicht nur zu vermeiden, den Speicher zu berühren, der vom aktuellen Wert nicht benötigt wird, Aber in der Lage zu sein, zu sagen, wie viele Bytes gelesen werden sollen, und den gesamten Arbeitsspeicher zur Laufzeit auf magische Weise zu verschieben, damit eine seltsame philosophische Idee der Raumeffizienz (ungeachtet der gigantischen Leistungskosten!) erfüllt wird ... Nur Anweisungen mit geringerem Platzbedarf gewonnen zu haben Das nicht lösen. Was eine CPU / ein Betriebssystem dafür tun müsste, wäre so komplex, dass es die Frage IMO am klarsten beantwortet.
underscore_d
1
Sie können jedoch nicht wirklich "Speicher speichern" in den Registern. Wenn Sie nicht versuchen, etwas Seltsames zu tun, indem Sie AH und AL missbrauchen, können Sie ohnehin nicht mehrere unterschiedliche Werte in demselben Universalregister haben. Lokale Variablen bleiben häufig in den Registern und gehen nie in den Arbeitsspeicher, wenn dies nicht erforderlich ist.
Meneldal