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 myInt
würde ich mit meinem Compiler 4 Bytes belegen. Der tatsächliche Wert 255
kann jedoch mit nur 1 Byte dargestellt werden. Warum sollte also myInt
nicht 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?
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.unsigned int
Wert.std::vector<X>
hat immer die gleiche Größe, dhsizeof(std::vector<X>)
ist eine Konstante zur Kompilierungszeit.Antworten:
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:
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:
Kompiliert (mit GCC 8.1 und der
-fomit-frame-pointer -O3
Einfachheit halber) zu:das heisst:
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 konvertierenimul
), die sehr schnell istBearbeiten: 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
mit dem entsprechenden Code für eine nicht standardmäßige Breite
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
oderuint32_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.
quelle
git
den 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.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.
quelle
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.
quelle
int
speichern einige die Anzahl der Elemente in diesem Array. Dasint
selbst wird wieder eine feste Größe haben.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:
Wenn Sie den Computer einfach anweisen, das erste Byte auf dem Band zu betrachten,
xx
woher weiß er dann, ob der Typ dort stoppt oder mit dem nächsten Byte fortfährt? Wenn Sie eine Zahl wie255
(hexadezimalFF
) oder eine Zahl wie65535
(hexadezimalFFFF
) haben, ist das erste Byte immerFF
.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 machex = y
? Wennint
die 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, wenny
ein Argument von einem anderen Code übergeben wird, der separat kompiliert wurde?quelle
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.
quelle
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.
quelle
56
und 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.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.
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:
struct
? Zufällige Adressierung!Was bedeutet, dass Sie im Wesentlichen den folgenden Kompromiss haben:
Feste Größentypen ODER lineare Speicherscans
quelle
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.
quelle
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.
quelle
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.
quelle
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.
quelle
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.
quelle
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 nehmensizeof(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 zuweisenstd::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
int
bei einer Implementierung 4 Byte lang sind und Siea
als Zeiger oder Array vonint
Werten deklarieren , wird diesa[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
char
und hat die Breite 1. Dadurch können ASCII-Zeichenfolgen als gültige UTF-8-Zeichenfolge interpretiert werden und viele Bibliothekscodes wiestrlen()
und könnenstrncpy()
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 int
undlong 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.
quelle
In erster Linie aufgrund von Ausrichtungsanforderungen.
Wie pro basic.align / 1 :
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.
quelle
Es kann weniger sein. Betrachten Sie die Funktion:
es wird zu Assembler-Code kompiliert (g ++, x64, Details entfernt)
Hier
bar
und ambaz
Ende verwenden Sie null Bytes zur Darstellung.quelle
Weil du es so oft benutzt hast. Bei Verwendung von a
unsigned int
schreiben 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 Sieunsigned char
stattdessen 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
quelle
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.
quelle
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. :) :)
quelle
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
char
in a umwandeln möchtenint
, 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.
quelle