Structures



  • Hi liebe Community .

    Ich habe eine simple Frage: Kann man sich in jeder erdenklichen Situation darauf verlassen, dass die member einer structure in genau der in der Deklaration festgelegten Reihenfolge und immer direkt hintereinander im Speicher abgelegt werden?

    Ich habe schon geprüft, ob man über folgende structure mit einem Pointer iterieren kann mit der Prognose, auf die einzelnen member zugreifen zu können.

    struct s{
    int a;
    int b;
    int c;
    int d;
    int e;
    };

    Wenn ich nun einen Pointer auf einen int deklariere und die Adresse des ersten members einer structure dieses Typs (a) hineinschreibe, kann ich über die structure iterieren, ähnlich wie über ein int-Array.

    Funktionierte in meinem Test, jedoch beantwortet das nicht die Frage, ob das in jedem erdenklichen Fall möglich ist - oder ob es doch spezielle Fälle gibt, in denen der Compiler unterschiedliche member derselben structure in weiter entfernte Speicherbereiche schreibt (was ich als etwas unpassend zur Philosophie von C bewerten würde, aber meine Meinung hat nicht viel Aussagekraft.)

    Bin sehr gespannt, danke schonmal im Voraus!



  • nein inbesondere betriebssystem- (bzw. compiler- ) übergreifend funktioniert das nur manchmal. du kannst dich also nicht darauf verlassen.



  • Nein, das gibt der Standart nicht her. Hintereinander in der Deklarationsreihenfolge im Speicher liegen ja, aber es kann padding dazwischen sein. Iterieren ist auch nicht drin, da das nur auf Arrays definiert ist.



  • @Joshuah sagte in Structures:

    ... oder ob es doch spezielle Fälle gibt, in denen der Compiler unterschiedliche member derselben structure in weiter entfernte Speicherbereiche schreibt ...

    Das ist leider der Normalfall. Um padding und alignment zu unterdrücken, gibt es '#pragma pack(1)' (das kennen GCC und MS-C)
    Siehe hier: https://stackoverflow.com/questions/3318410/pragma-pack-effect



  • @Joshuah Die Reihenfolge bleibt.
    Der Compiler darf Lücken einbauen.
    Wenn jedoch derselbe Variablentyp aufeinander folgt ist das nicht nötig.

    Um Lücken zu vermeiden ist es ganz praktisch, mit die Element der Größe nach in der struct zu deklarieren (von groß nach klein).

    Die Adresse innerhalb der struct bekommst du mit dem Makro OFFSETOF



  • @DirkB sagte in Structures:

    Wenn jedoch derselbe Variablentyp aufeinander folgt ist das nicht nötig.

    Auch hierbei können Paddingbytes vorkommen.
    OFFSETOF liefert auch keine Adresse sondern den Byteabstand vom struct-Beginn.
    #pragma pack und Konsorten sind kein Sprachstandard.
    Ein standardkonformer Compiler kann nach jedem struct-Element eine beliebige Zahl von Paddingbytes einfügen, außer am Beginn; d.h. es ist garantiert, dass die struct-Adresse immer exakt der Adresse des ersten Elements entspricht.
    Ein Compiler braucht auch nie gleiche Paddingbytes für gleichdefinierte structs erzeugen,

    struct bla {char c;int i;}
    ... viele Zeilen Code
    struct bla1 {char c;int i;}
    

    führt also nicht garantiert zum (von Nicht-Profis) erwarteten identischen Memorylayout.



  • @DirkB sagte in Structures:

    Um Lücken zu vermeiden ist es ganz praktisch, mit die Element der Größe nach in der struct zu deklarieren (von groß nach klein).

    Diese Optimierungsaufgabe ist wohl mit dem "Rucksack-Problem" verwandt. 😺
    https://en.wikipedia.org/wiki/Knapsack_problem



  • Übrigens gibt es in clang-tidy den Checker clang-analyzer-optin.performance.Padding, der kann ungünstiges Padding erkennen. Das gibt dann so Meldungen wie

    error: Excessive padding in 'struct RooAbsReal::PlotOpt' (38 padding bytes, where 6 is optimal). 
    Optimal fields order: 
    drawOptions, 
    scaleFactor, 
    (....)
    consider reordering the fields or adding explicit padding members 
      struct PlotOpt {
    	 ^
    include/RooAbsReal.h:416:10: note: Excessive padding in 'struct RooAbsReal::PlotOpt' (38 padding
    bytes, where 6 is optimal). Optimal fields order: drawOptions, scaleFactor,  (...)```


  • @wob sagte in Structures:

    Übrigens gibt es in clang-tidy den Checker clang-analyzer-optin.performance.Padding, der kann ungünstiges Padding erkennen. Das gibt dann so Meldungen wie

    error: Excessive padding in 'struct RooAbsReal::PlotOpt' (38 padding bytes, where 6 is optimal). 
    Optimal fields order: 
    drawOptions, 
    scaleFactor, 
    (....)
    consider reordering the fields or adding explicit padding members 
      struct PlotOpt {
    	 ^
    include/RooAbsReal.h:416:10: note: Excessive padding in 'struct RooAbsReal::PlotOpt' (38 padding
    bytes, where 6 is optimal). Optimal fields order: drawOptions, scaleFactor,  (...)```
    

    Ich bin fasziniert. Wusste wirklich nicht, dass man sich auch darüber Gedanken machen sollte😅



  • Hi Leute!

    Also erstmal: Sorry, dass ich erst jetzt antworte, und vielen Dank für die ausführliche Hilfe!

    Die Frage, die ich mir jetzt stelle ist, wie der Compiler herausfindet, wie er auf die Member eines structs treffsicher zugreift, wenn zwischen ihnen beliebig viele Paddingbytes liegen können. legt er die Menge an Paddingbytes in einer struct im Moment ihrer Deklaration fest? Dann würde die Sache immer eindeutig sein im Sinne von "Okay, ich habe einen 4-Byte-int, dann 3 Bytes Füllmaterial. Die Adresse des nächsten Elementes (von variablen genau dieser structure) ist also immer 6 Bytes hinter der Adresse des 1. Elements."



  • @Joshuah
    Der Compiler kennt deine Zielplattform und weiß, dass die CPU schnell auf Daten zugreifen kann, die auf einer (WORD/DWORD/QWORD) Adresse liegen, je nach Zielplattform. Also versucht er, die Member des struct auf die jeweiligen Adressen auszurichten und fügt daziwschen Padding Bytes ein. Auch wenn es im Standard keine Möglichkeit gibt das Padding zu beeinflussen bieten fast alle Compiler eine eigene Lösung dafür an. Ich habe mir angewöhnt für den Fall, dass die Strukturgröße fest sein muss static_asserts einzubauen:

    #pragma pack( push, 1)
    struct MyData
    {
       int32_t a1;
       int8_t c;
       int8_t d;
       double v;
    };
    #pragma pack (pop)
    static_assert( sizeof( Mydata ) == 14, "sizeof( MyData ) must be 14!" );
    
    Edit:
    C++11 Datentypen ergänzt.


  • Ah okay! Ja, ich kenne den Nutzen von "Padding" nicht und muss zugeben, dass ich in diesem Thread das erste Mal bewusst davon gelesen habe. Das bedeutet, dass die Adressen im Speicher nach Verwendung eingeteilt werden?
    Auch "pragma pack" kenne ich nicht.
    Sagt mir alles noch nicht viel, ich werd mich da mal schlaumachen. Danke an alle! .

    Edit: Moment, wenn man die Zielplattform kennt und damit die Art, wie der Compiler structs arrangiert, kann man dann nicht die Anzahl der Paddingbytes vorhersagen?



  • @Joshuah
    Klar kann man das, man muss nur seinen Compiler kennen. Unser 32 Bit Compiler richtet die Member auf DWORD Grenzen aus, aber das ist compilerabhängig und kann sich sogar von Version zu Version ändern. Oft sind solche Dinge aber auch in den Projekteinstellungen festgelegt und lassen sich dort einstellen.



  • @Joshuah sagte in Structures:

    Ah okay! Ja, ich kenne den Nutzen von "Padding" nicht und muss zugeben, dass ich in diesem Thread das erste Mal bewusst davon gelesen habe. Das bedeutet, dass die Adressen im Speicher nach Verwendung eingeteilt werden?

    naja im prinzip bedeutet das, dass dein 64-bit rechner intern besonders gut (schnell) auf 64-bit variablen zugreifen kann und der compiler dann quasi intern einzelne kleinere variablen vergrößert bzw. die adressen halt so anpasst. allgemein brauchst du dich da nicht weiter drum zu kümmern, aber wenn die struktur auf deinem windows-rechner 120 bytes groß und die gleiche struktur auf dem unix-rechner 126 bytes groß ist, hast du halt ein problem, wenn du die struktur in einem rutsch über das netzwerk schickst.

    Edit: Moment, wenn man die Zielplattform kennt und damit die Art, wie der Compiler structs arrangiert, kann man dann nicht die Anzahl der Paddingbytes vorhersagen?

    ja aber ich würde mich halt nicht darauf verlassen, dass das immer so bleibt.



  • @Joshuah Damit das funktioniert muss du neben dem bereits erwähnten Structure-Packing auch noch sicherstellen dass die Liste der Member nicht durch Access-Specifier (private:, protected:, public:) unterbrochen werden. Denn Sektionen die durch Access-Specifier "getrennt" sind darf der Compiler umordnen.

    Weiters ist zu beachten dass solche Konstrukte

    struct s{
    int a;
    int b;
    int c;
    int d;
    int e;
    };
    
    void foo() {
        s S = ...;
        // assuming s has no padding
        int* p = &S.a;
        int a = *p; //OK
        p++; // ???
        int b = *p; // ???
    }
    

    nicht unbedingt OK sind. Ich kenne den Standard dazu nicht gut genug um zu dem konkreten Beispiel was konkretes sagen zu könne. Allerdings gibt es hier zumindest ein potentielles Problem, und das ist dass es in C++ nicht reicht die richtige Adresse für irgendwas zu haben, sondern man muss die Adresse auf "legalem" Weg bekommen haben. Und ob p++ hier legal ist... keine Ahnung.

    Was auf jeden Fall OK gehen sollte, ist wenn du den Umweg über memcpy machst:

    struct s{
    int a;
    int b;
    int c;
    int d;
    int e;
    };
    
    void foo() {
        s S = ...;
        // assuming s has no padding
        unsigned char* p = reinterpret_cast<unsigned char*>(&S.a);
        int a;
        memcpy(&a, p, sizeof(int));
        p += sizeof(int);
        int b;
        memcpy(&b, p, sizeof(int));
    }
    


  • @Wade1234 sagte in Structures:

    naja im prinzip bedeutet das, dass dein 64-bit rechner intern besonders gut (schnell) auf 64-bit variablen zugreifen kann

    Das hängt mehr von der Busbreite des Datenbuses zum Speicher ab.

    Wenn der Bus 16-Bit breit ist, dann hat der nur gerade Adressen.
    Wird ein Byte (8-Bit) geladen, dann greift die CPU auf alle 16-Bit zu, liest aber nur 8-Bit in das Register.

    Wenn jetzt aber ein Word (16-Bit) auf einer ungeraden Adresse liegt, dann muss die CPU zweimal lesen.

    Adresse  | LowByte | HiByte |
       0000  | Byte 0  | Byte 1 |
       0002  | Byte 2  | Byte 3 |
       0004  | Word   0         |
       0006  | Word   1         |
       0008  | Byte 4  | Word 2 |
       000A  | Word 2  |        |
    

    Um das zu umgehen, legt der Compiler die Variablen gerne auf solche Adressen, dass die Daten mit einem Zugriff geladen werden können.
    Das ist stark von der Architektur abhängig. Manche können 16-Bit Werte gar nicht von ungeraden Adressen laden.



  • Ähm @hustbaer, ich weiß ja net ob es hilft, aber ich programmiere in C. Nicht in C++, davon habe ich keine Ahnung und ich werde zumindest im nächsten halben Jahr auch nicht umsteigen. Trotzdem nett



  • @DirkB sagte in Structures:

    Das hängt mehr von der Busbreite des Datenbuses zum Speicher ab.

    ja also ich ging jetzt davon aus, dass bei gängigen 64-bit rechnern daten- und adressbus 64-bit breit sind (auch wenn letzterer wahrscheinlich erst einmal nicht ausgeschöpft werden kann).

    @Joshuah sagte in Structures:

    davon habe ich keine Ahnung und ich werde zumindest im nächsten halben Jahr auch nicht umsteigen.

    sehr löblich! 🙂



  • @Joshuah Upps, übersehen dass die Frage ja im C Unterforum ist. Ob es in C auch so üble Stolpersteine gibt wie in C++ weiss ich nicht. Also so Stolpersteine wie eben dass es auch wichtig ist wie man zu einem Zeiger gekommen ist, und nicht nur ob man die richtige Adresse hat.



  • @DirkB sagte in Structures:

    Wenn jetzt aber ein Word (16-Bit) auf einer ungeraden Adresse liegt, dann muss die CPU zweimal lesen.

    Und shiften und odern, um sich das Word zusammenzubasteln. Daher sollte man bei zeitkritischen Programen auf sowas wie #pragma pack(1) besser verzichten.


Log in to reply