Warum gibt der sizeof
Operator eine Größe zurück, die für eine Struktur größer ist als die Gesamtgröße der Elemente der Struktur?
Warum gibt der sizeof
Operator eine Größe zurück, die für eine Struktur größer ist als die Gesamtgröße der Elemente der Struktur?
Antworten:
Dies liegt an der Auffüllung, die hinzugefügt wurde, um Ausrichtungsbeschränkungen zu erfüllen. Die Ausrichtung der Datenstruktur wirkt sich sowohl auf die Leistung als auch auf die Richtigkeit der Programme aus:
SIGBUS
) ein schwerer Fehler sein .Hier ist ein Beispiel mit typischen Einstellungen für einen x86-Prozessor (alle verwendeten 32- und 64-Bit-Modi):
struct X
{
short s; /* 2 bytes */
/* 2 padding bytes */
int i; /* 4 bytes */
char c; /* 1 byte */
/* 3 padding bytes */
};
struct Y
{
int i; /* 4 bytes */
char c; /* 1 byte */
/* 1 padding byte */
short s; /* 2 bytes */
};
struct Z
{
int i; /* 4 bytes */
short s; /* 2 bytes */
char c; /* 1 byte */
/* 1 padding byte */
};
const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */
Man kann die Größe von Strukturen minimieren, indem man Elemente nach Ausrichtung sortiert (die Sortierung nach Größe reicht für die Basistypen aus) (wie die Struktur Z
im obigen Beispiel).
WICHTIGER HINWEIS: Sowohl der C- als auch der C ++ - Standard geben an, dass die Strukturausrichtung implementierungsdefiniert ist. Daher kann jeder Compiler Daten unterschiedlich ausrichten, was zu unterschiedlichen und inkompatiblen Datenlayouts führt. Aus diesem Grund ist es beim Umgang mit Bibliotheken, die von verschiedenen Compilern verwendet werden, wichtig zu verstehen, wie die Compiler Daten ausrichten. Einige Compiler verfügen über Befehlszeileneinstellungen und / oder spezielle #pragma
Anweisungen, um die Einstellungen für die Strukturausrichtung zu ändern.
Packen und Byte-Alignment, wie in den C-FAQ hier beschrieben :
Es ist für die Ausrichtung. Viele Prozessoren können nicht auf 2- und 4-Byte-Mengen zugreifen (z. B. Ints und Long-Ints), wenn sie in jeder Hinsicht überfüllt sind.
Angenommen, Sie haben diese Struktur:
struct { char a[3]; short int b; long int c; char d[3]; };
Nun könnte man denken, dass es möglich sein sollte, diese Struktur wie folgt in den Speicher zu packen:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
Aber es ist viel, viel einfacher für den Prozessor, wenn der Compiler es so anordnet:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
Beachten Sie in der gepackten Version, dass es für Sie und mich zumindest ein bisschen schwierig ist zu sehen, wie sich die Felder b und c drehen? Kurz gesagt, es ist auch für den Prozessor schwierig. Daher füllen die meisten Compiler die Struktur (wie mit zusätzlichen, unsichtbaren Feldern) wie folgt auf:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
then &s.a == &s
und haben &s.d == &s + 12
(angesichts der in der Antwort gezeigten Ausrichtung). Der Zeiger wird nur gespeichert, wenn die Arrays eine variable Größe haben (z. B. a
wurde char a[]
anstelle von deklariert char a[3]
), aber dann müssen die Elemente an einer anderen Stelle gespeichert werden.
Wenn Sie möchten, dass die Struktur beispielsweise mit GCC eine bestimmte Größe hat, verwenden Sie __attribute__((packed))
.
Unter Windows können Sie die Ausrichtung auf ein Byte festlegen, wenn Sie den cl.exe-Compier mit der Option / Zp verwenden .
Normalerweise ist es für die CPU einfacher, auf Daten zuzugreifen, die ein Vielfaches von 4 (oder 8) sind, abhängig von der Plattform und auch vom Compiler.
Es ist also im Grunde eine Frage der Ausrichtung.
Sie müssen gute Gründe haben, dies zu ändern.
Dies kann auf die Ausrichtung und das Auffüllen von Bytes zurückzuführen sein, sodass die Struktur eine gerade Anzahl von Bytes (oder Wörtern) auf Ihrer Plattform aufweist. Zum Beispiel in C unter Linux die folgenden 3 Strukturen:
#include "stdio.h"
struct oneInt {
int x;
};
struct twoInts {
int x;
int y;
};
struct someBits {
int x:2;
int y:6;
};
int main (int argc, char** argv) {
printf("oneInt=%zu\n",sizeof(struct oneInt));
printf("twoInts=%zu\n",sizeof(struct twoInts));
printf("someBits=%zu\n",sizeof(struct someBits));
return 0;
}
Haben Sie Mitglieder, deren Größe (in Bytes) 4 Bytes (32 Bit), 8 Bytes (2x 32 Bit) bzw. 1 Byte (2 + 6 Bit) beträgt. Das obige Programm (unter Linux mit gcc) gibt die Größen 4, 8 und 4 aus - wobei die letzte Struktur so aufgefüllt wird, dass es sich um ein einzelnes Wort handelt (4 x 8-Bit-Bytes auf meiner 32-Bit-Plattform).
oneInt=4
twoInts=8
someBits=4
:2
und geben :6
tatsächlich 2 und 6 Bit an, in diesem Fall keine vollen 32-Bit-Ganzzahlen. Einige Bits.x, die nur 2 Bits sind, können nur 4 mögliche Werte speichern: 00, 01, 10 und 11 (1, 2, 3 und 4). Macht das Sinn? Hier ist ein Artikel über die Funktion: geeksforgeeks.org/bit-fields-c
Siehe auch:
für Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
und GCC behaupten Kompatibilität mit dem Compiler von Microsoft:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
Beachten Sie zusätzlich zu den vorherigen Antworten, dass es in C ++ unabhängig von der Verpackung keine Garantie für die Bestellung von Mitgliedern gibt . Compiler können (und sicherlich auch) Mitglieder der virtuellen Tabellenzeiger und Basisstrukturen zur Struktur hinzufügen. Selbst die Existenz einer virtuellen Tabelle wird durch den Standard nicht sichergestellt (die Implementierung eines virtuellen Mechanismus ist nicht spezifiziert), und daher kann man schließen, dass eine solche Garantie einfach unmöglich ist.
Ich bin ganz sicher , Mitglied Ordnung ist in C garantiert , aber ich würde nicht auf sie zählen, wenn eine plattformübergreifende oder Cross-Compiler - Programm zu schreiben.
Die Größe einer Struktur ist aufgrund der sogenannten Packung größer als die Summe ihrer Teile. Ein bestimmter Prozessor hat eine bevorzugte Datengröße, mit der er arbeitet. Die bevorzugte Größe der meisten modernen Prozessoren beträgt 32 Bit (4 Byte). Der Zugriff auf den Speicher, wenn sich Daten an dieser Art von Grenze befinden, ist effizienter als Dinge, die diese Größengrenze überschreiten.
Zum Beispiel. Betrachten Sie die einfache Struktur:
struct myStruct
{
int a;
char b;
int c;
} data;
Wenn die Maschine eine 32-Bit-Maschine ist und Daten an einer 32-Bit-Grenze ausgerichtet sind, sehen wir ein unmittelbares Problem (vorausgesetzt, keine Strukturausrichtung). In diesem Beispiel nehmen wir an, dass die Strukturdaten an der Adresse 1024 beginnen (0x400 - beachten Sie, dass die niedrigsten 2 Bits Null sind, sodass die Daten an einer 32-Bit-Grenze ausgerichtet sind). Der Zugriff auf data.a funktioniert einwandfrei, da er an einer Grenze beginnt - 0x400. Der Zugriff auf data.b funktioniert ebenfalls einwandfrei, da er sich an der Adresse 0x404 befindet - einer weiteren 32-Bit-Grenze. Eine nicht ausgerichtete Struktur würde jedoch data.c an die Adresse 0x405 setzen. Die 4 Bytes von data.c befinden sich bei 0x405, 0x406, 0x407, 0x408. Auf einer 32-Bit-Maschine würde das System während eines Speicherzyklus data.c lesen, aber nur 3 der 4 Bytes erhalten (das 4. Byte befindet sich an der nächsten Grenze). Das System müsste also einen zweiten Speicherzugriff durchführen, um das 4. Byte zu erhalten.
Wenn der Compiler nun anstelle von data.c an der Adresse 0x405 die Struktur mit 3 Bytes auffüllt und data.c an der Adresse 0x408 platziert, benötigt das System nur 1 Zyklus zum Lesen der Daten, wodurch die Zugriffszeit auf dieses Datenelement verkürzt wird um 50%. Durch das Auffüllen wird die Speichereffizienz gegen die Verarbeitungseffizienz ausgetauscht. Angesichts der Tatsache, dass Computer sehr viel Speicher haben können (viele Gigabyte), halten die Compiler den Austausch (Geschwindigkeit über Größe) für angemessen.
Leider wird dieses Problem zum Killer, wenn Sie versuchen, Strukturen über ein Netzwerk zu senden oder sogar die Binärdaten in eine Binärdatei zu schreiben. Das Auffüllen zwischen Elementen einer Struktur oder Klasse kann die an die Datei oder das Netzwerk gesendeten Daten stören. Um tragbaren Code zu schreiben (einer, der an mehrere verschiedene Compiler gesendet wird), müssen Sie wahrscheinlich auf jedes Element der Struktur separat zugreifen, um das richtige "Packen" sicherzustellen.
Andererseits haben verschiedene Compiler unterschiedliche Fähigkeiten, um das Packen von Datenstrukturen zu verwalten. In Visual C / C ++ unterstützt der Compiler beispielsweise den Befehl #pragma pack. Auf diese Weise können Sie das Packen und Ausrichten von Daten anpassen.
Zum Beispiel:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
Ich sollte jetzt die Länge von 11 haben. Ohne das Pragma könnte ich zwischen 11 und 14 sein (und für einige Systeme sogar 32), abhängig von der Standardverpackung des Compilers.
#pragma pack
. Wenn Mitglieder bei ihrer Standardausrichtung zugewiesen werden, würde ich im Allgemeinen sagen, dass die Struktur nicht gepackt ist.
Dies ist möglich, wenn Sie die Ausrichtung der Struktur implizit oder explizit festgelegt haben. Eine Struktur, die auf 4 ausgerichtet ist, ist immer ein Vielfaches von 4 Bytes, selbst wenn die Größe ihrer Mitglieder etwas ist, das kein Vielfaches von 4 Bytes ist.
Außerdem kann eine Bibliothek unter x86 mit 32-Bit-Ints kompiliert werden, und wenn Sie ihre Komponenten in einem 64-Bit-Prozess vergleichen, erhalten Sie ein anderes Ergebnis, wenn Sie dies von Hand tun.
C99 N1256 Standardentwurf
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 Die Größe des Operators :
3 Bei Anwendung auf einen Operanden mit Struktur- oder Vereinigungstyp ergibt sich die Gesamtzahl der Bytes in einem solchen Objekt, einschließlich interner und nachfolgender Auffüllung.
6.7.2.1 Struktur- und Vereinigungsspezifizierer :
13 ... In einem Strukturobjekt befindet sich möglicherweise eine unbenannte Auffüllung, jedoch nicht am Anfang.
und:
15 Am Ende einer Struktur oder Vereinigung befindet sich möglicherweise eine unbenannte Polsterung.
Die neue C99-Funktion für flexible Array-Elemente ( struct S {int is[];};
) kann sich auch auf das Auffüllen auswirken:
16 Als Sonderfall kann das letzte Element einer Struktur mit mehr als einem benannten Element einen unvollständigen Array-Typ haben. Dies wird als flexibles Array-Mitglied bezeichnet. In den meisten Situationen wird das flexible Array-Mitglied ignoriert. Insbesondere ist die Größe der Struktur so, als ob das flexible Array-Element weggelassen worden wäre, mit der Ausnahme, dass es möglicherweise mehr nachlaufende Polsterung aufweist, als die Auslassung implizieren würde.
Anhang J Portabilitätsprobleme bekräftigt:
Folgendes ist nicht spezifiziert: ...
- Der Wert des Auffüllens von Bytes beim Speichern von Werten in Strukturen oder Vereinigungen (6.2.6.1)
C ++ 11 N3337 Standardentwurf
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 Größe von :
2 Bei Anwendung auf eine Klasse ergibt sich die Anzahl der Bytes in einem Objekt dieser Klasse, einschließlich aller Auffüllungen, die zum Platzieren von Objekten dieses Typs in einem Array erforderlich sind.
9.2 Klassenmitglieder :
Ein Zeiger auf ein Strukturobjekt mit Standardlayout, das mit einem reinterpret_cast geeignet konvertiert wurde, zeigt auf sein ursprüngliches Element (oder, wenn dieses Element ein Bitfeld ist, auf die Einheit, in der es sich befindet) und umgekehrt. [Hinweis: In einem Strukturobjekt mit Standardlayout kann es daher zu unbenannten Auffüllungen kommen, jedoch nicht zu Beginn, um eine angemessene Ausrichtung zu erreichen. - Endnote]
Ich kenne nur genug C ++, um den Hinweis zu verstehen :-)
Zusätzlich zu den anderen Antworten kann eine Struktur virtuelle Funktionen haben (aber normalerweise nicht). In diesem Fall enthält die Größe der Struktur auch den Platz für das vtbl.
Die Sprache C lässt dem Compiler einige Freiheiten hinsichtlich der Position der Strukturelemente im Speicher:
Die C-Sprache bietet dem Programmierer eine gewisse Sicherheit für das Elementlayout in der Struktur:
Probleme im Zusammenhang mit der Ausrichtung der Elemente:
So funktioniert die Ausrichtung:
ps Ausführlichere Informationen finden Sie hier: "Samuel P. Harbison, Guy L. Steele CA Reference, (5.6.2 - 5.6.7)"
Die Idee ist, dass Operanden aus Gründen der Geschwindigkeit und des Cache von Adressen gelesen werden sollten, die auf ihre natürliche Größe ausgerichtet sind. Um dies zu erreichen, füllt der Compiler die Strukturelemente so auf, dass das folgende Element oder die folgende Struktur ausgerichtet werden.
struct pixel {
unsigned char red; // 0
unsigned char green; // 1
unsigned int alpha; // 4 (gotta skip to an aligned offset)
unsigned char blue; // 8 (then skip 9 10 11)
};
// next offset: 12
Die x86-Architektur war immer in der Lage, falsch ausgerichtete Adressen abzurufen. Es ist jedoch langsamer und wenn die Fehlausrichtung zwei verschiedene Cache-Zeilen überlappt, werden zwei Cache-Zeilen entfernt, wenn ein ausgerichteter Zugriff nur eine entfernen würde.
Einige Architekturen müssen sich tatsächlich auf falsch ausgerichtete Lese- und Schreibvorgänge und frühe Versionen der ARM-Architektur (die sich zu allen heutigen mobilen CPUs entwickelt hat) beschränken. Nun, sie haben tatsächlich nur schlechte Daten für diese zurückgegeben. (Sie ignorierten die niederwertigen Bits.)
Beachten Sie schließlich, dass die Cache-Zeilen beliebig groß sein können und der Compiler nicht versucht, diese zu erraten oder einen Kompromiss zwischen Leerzeichen und Geschwindigkeit einzugehen. Stattdessen sind die Ausrichtungsentscheidungen Teil des ABI und stellen die minimale Ausrichtung dar, die schließlich eine Cache-Zeile gleichmäßig ausfüllt.
TL; DR: Ausrichtung ist wichtig.