12 ms·
The Lost Art of Structure Packing (2018)
- dorianh 6y agoI always assumed that in C, a given structure would always have the same memory layout, no matter what the compiler is (as long as the compilers target the same architecture of course). I always assumed that in C++, the memory layout could change a lot between compilers (the location of the pointer to the vtable for example). Do you know if it's true, and if the layout do change, could you give an example?
- saagarjha 6y ago> I always assumed that in C, a given structure would always have the same memory layout, no matter what the compiler is (as long as the compilers target the same architecture of course). Usually, but not always. In most cases there is one efficient way to pack the structure and still maintain member alignment, but there's not requirement that the amount of padding looks like this. > I always assumed that in C++, the memory layout could change a lot between compilers (the location of the pointer to the vtable for example). Do you know if it's true, and if the layout do change, could you give an example? Yes, once you have a non-POD type the memory layout can be fairly arbitrary as you cannot really inspect it and compilers are free to lay it out as they wish.
- ChrisSD 6y agoMost platforms have have system level C APIs that are exposed to userspace. When using these APIs it's necessary to layout structures as they expect. Therefore all compilers on the same platform (OS+arch) would usually produce the same layout for structs so that they are compatible. However this isn't universally true. Some platforms might not have a well defined C ABI.
- adrianN 6y agoIt would be nice if there was an annotation that just lets the compiler do all the optimization for me for the cases where I don't care about the memory layout of the struct. Just like the Rust compiler can do without repr(C)
- elteto 6y agoWait, forgive my ignorance, isn’t this the default? If you don’t care about the memory layout then won’t the compiler reorg / pad structure members to fulfill alignment?
- virgilp 6y agoPad - yes, typically. Reorg - seldom.
- klodolph 6y agoReorg is prohibited by C standard.
- virgilp 6y agoI know, it wasn't obvious to me that elteto talked exclusively about C/C++. AFAIK few -if any- compilers/VMs take it upon themselves to reorder struct members (even outside C/C++ world).
- leni536 6y agoEven though struct member reordering is prohibited, the order of std::tuple elements are not fixed by the standard. There could be an stdlib implementation that used this fact to reorder tuple members optimally. AFAIK there are PoC 3rd party implementations for such tuples.
- saagarjha 6y agoBoth Swift and Rust have an unspecified structure ordering by default, which almost always boils down to "the compiler will lay them out in the order specified, except if you leave enough padding to fit a member, in which case it'll reorder the elements for you".
- gpderetta 6y agoIt is complicated. Because of the as-if rule, a compiler can do anything it wants as long as a conforming program can't tell the difference. So for example if a compiler can prove that a program doesn't compare fiel addresses, doesn't cast pointer to a struct to its first member, etc it can reorder as it pleases. Turns out it is very hard to prove, often requires whole program optimization and the gains are questionable, so it is seldom done. Both clang and gcc had such an optimization pass in the past but it got dropped.
- munchbunny 6y agoIf you do any development that requires touching Win32, structure packing and memory alignment is still very real. I was recently doing that in the context of improving a language’s library support for something that had to go through the OS SDK. I don’t miss the days when that stuff was the norm.
- BubRoss 6y agoI have done some win32 programming but haven't encountered struct packing or alignment being an issue, where does that pop up?
- gambiting 6y agoI work in video games, and very recently we had a sneaky bug in one of our AAA titles(that was already out!), where(in huge simplification) we had a struct that looked like: struct Obj { int foo; bool bar; } then we were storing those in a custom hashmap using these as keys, where the hashing function was basically hashing bits of each stored object, without any awareness of what's in the object. The bug was found when someone did something like: void DoSomething(Obj obj) { if(!map.has(obj)) map.add(obj, new Whatever()); map[obj].blah(); //CRASH null pointer exception } I was like.....well, if there is no key "obj" in the map, we insert one....and yet literally one line after it doesn't have a value for that key??? How can this be? Well, it can be because even though the struct looks like it takes 5 bytes, in reality it's 8 bytes because it's getting padded. So a naive hashing method that just looks at bits is hashing your 5 bytes of actual data + 3 bytes of garbage, which means that two "identical" objects are very unlikely to actually produce the same hash. C++20 now has a "hashable" concept to help with this, but it still requires the programmer to be aware of structure packing.
- flohofwoe 6y agoIt's not just the different "under the hood size" that's a problem in this situation, but alignment-padding bytes added by the compiler inbetween struct members will have random junk data in them, the compiler will not zero-initialize padding bytes, unless you explicitely memset() the struct (and even then I wouldn't count on that the padding bytes aren't "tainted" later). E.g. if you initialize a C struct or C++ object the "usual way": Obj obj = { }; There will most definitely be junk in the padding bytes.
- joosters 6y agoTLDR: When defining a struct, put the biggest items first. This works well in almost all cases. If you need to squeeze out all padding, use compiler directives like '#pragma pack', but be aware of the performance implications.
- kazinator 6y agoI've documented how GCC does bitfield packing in the TXR reference manual. Or rather, the abstract algorithm used in the FFI to replicate it. https://www.nongnu.org/txr/txr-manpage.html#N-027D075C https://www.nongnu.org/txr/txr-manpage.html#N-027D075C This is the result of empirical investigation. The description also covers allocation of non-bitfields (paragraph 3) and the padding of the structure (paragraph 9) which require few words. I felt that the bitfield handling is so obscure that it had to be documented in detail. If someone is to know exactly what the layout will be, the documentation can't just be "oh, it will behave like a GCC struct". Well, what will that do? That is not adequately documented anywhere. If I have to work with bitfields in just C, I can use that as a reference to understand what the compiler will do (at least if it's anything compatible with GCC).
- carapace 6y agoThat's awesome! It's the kind of detail that, when you need it you really need it, but it's often so hard to find, or locked in some proprietary deal. I can barely imagine the amount of work it must have taken to nail all that down. Congratulations, and thank you!
- kazinator 6y agoI tried numerous cases, and looked at the memory, and also read and wrote the structures with FFI to make sure they match what the C compiler is putting out, and fixed bugs along the way. For big endian investiations, I borrowed the big endian PPC machine courtesy of the GCC Compile Farm project. The details are not obvious; like the fact that a zero width bitfield like "int : 0" that appears etween two members that are not bitfields actually does something. E.g. this has size 5: struct { char c1; int : 0; // zero-width bit-field must be unnamed char c2; }; This is basically because c1 is de-facto considered to be 8 allocated bits out of an int-wide cell, leaving 24 bits in that cell. The int : 0 sees that a field has been partially filled and so increments to the next int-wide field (according to my documented hypothesis). ISO C says (or did say in 1999) only this: "A bit-field declaration with no declarator, but only a colon and a width, indicates an unnamed bit-field.105) As a special case, a bit-field structure member with a width of 0 indicates that no further bit-field is to be packed into the unit in which the previous bit-field, if any, was placed." No "further bit-field" is to be packed, but in this example there is neither a previous nor next bit field. So you might expect that there is no effect. In the GCC model of "all allocated so far are just bits", it has an effect. Footnote 105 says just that "An unnamed bit-field structure member is useful for padding to conform to externally imposed layouts" which is more or less self-evident. Oh wow; I just realized that the empty bit-field has an effect if it is the last member also: struct { // now size 8! char c1; int : 0; char c2; int : 0; }; This is predicted by my documentation, but it should be spelled out in an explicit remark. It's a useful feature of GCC bit-fields because you can conform to certain external layouts without having to use bit-fields at all, other than the zero-width ones.
- throwaway_pdp09 6y agoIt's probably not talked about so much now because of the canard that memory is cheap, and because HLLs disguise things a bit too much and so mislead newbies, but to suggest it's lost is plain wrong.
- saagarjha 6y agoI think the main reason is that many new languages do this for you, at the cost of not guaranteeing a particular structure member order.
- WesolyKubeczek 6y agoSheesh, we're trying to be ABI-compatible over here.
- saagarjha 6y agoFor interoperability concerns within the language itself, this is usually solved by making the layout algorithm deterministic or passing around the aggregate around with an invisible pointer. When interacting with other languages, typically there's an attribute to ensure that layout matches declaration order.
- garjana 6y agoWhen I worked at a prop trading firm on a greenfield market data system this kind of optimization was very much on our minds. I assume others in this field also take care to pack structs.
- DagAgren 6y agoIt's 2020, do we really still need to pay attention to ESR?
- WesolyKubeczek 6y agoIt's been 5 minutes already, why pay attention to one DagAgren and their comments?
- DagAgren 6y agoMay I present my own achievements, such as: Not being an awful racist, and not spending any of my time defending pedophile rapists?
- WesolyKubeczek 6y agoAh, you're one of _those_ people.
- saagarjha 6y agoPeople can be "awful racists" but still have valuable contributions; the key is to separate the two. TempleOS was an interesting take on operating systems, for example, irrespective of the fact that Terry Davis was a paranoid person prone to racist rants.
- Natsu 6y agoI'm sort of surprised there are no tools for this. I understand why having the compiler reorder things could be bad, though it seems like there should be room to tell the compiler it's okay to repack it for minimum space, but I don't even see any mention of a source-level tool that would just sort the items in a struct for you. It seems like something like that could be useful rather than making programmers try to order their structs by hand.
- SlowRobotAhead 6y agoIf you just order top down in structures from pointers, 32s, 16s, arrays, 8s, it’s almost entirely done without thinking. There is almost never a difference to the user what order things are structured. Although to be fair this does get tricky with unions of structure over structure.
- Natsu 6y agoTrue, but that's the kind of drudgery that's best farmed off to computers. I see that the comment above mentions there is a tool for this that I simply wasn't aware of.
- smcameron 6y agoThere's pahole https://lwn.net/Articles/335942/ https://lwn.net/Articles/335942/
- hellofunk 6y agoBiggest surprise for me is learning that a pointer is a whopping eight bites! I have always mistakenly assumed they were small and just four bites.
- saagarjha 6y agoIt depends on the platform! On ILP64 and LP64 (and presumably P64, though I've never heard of this actually being used anywhere) they'll be 8 bytes, but not on most 32-bit architectures.
- flyingfences 6y agoIsn't that the _definition_ of a 32-bit (or 64-bit or 8-bit or however-many-bit) - that 32 bits is the length of a pointer?
- saagarjha 6y agoCertain strange platforms (arm64_32 for Apple Watch) run ILP32 on AArch64.
- grandinj 6y agoI wrote a clang plugin to look for opportunities across a 10M line codebase and there was surprisingly little to be found. Why? Because on 64-bit Linux, the current C++ ABI mandates quite large alignment, especially once you are embedding things inside other things. Packing is still sometimes useful in speeding things up, but tends to require bitfields and flattening structs inside structs into a single struct, etc.
- 0xDEEPFAC 6y agoLooks like what he really wants is to use Ada which has had much better support for low-level programming than C. Example: Word : constant := 4; -- storage element is byte, 4 bytes per word type State is (A,M,W,P); type Mode is (Fix, Dec, Exp, Signif); type Byte_Mask is array (0..7) of Boolean; type State_Mask is array (State) of Boolean; type Mode_Mask is array (Mode) of Boolean; type Program_Status_Word is record System_Mask : Byte_Mask; Protection_Key : Integer range 0 .. 3; Machine_State : State_Mask; Interrupt_Cause : Interruption_Code; Ilc : Integer range 0 .. 3; Cc : Integer range 0 .. 3; Program_Mask : Mode_Mask; Inst_Address : Address; end record; for Program_Status_Word use record System_Mask at 0*Word range 0 .. 7; Protection_Key at 0*Word range 10 .. 11; -- bits 8,9 unused Machine_State at 0*Word range 12 .. 15; Interrupt_Cause at 0*Word range 16 .. 31; Ilc at 1*Word range 0 .. 1; -- second word Cc at 1*Word range 2 .. 3; Program_Mask at 1*Word range 4 .. 7; Inst_Address at 1*Word range 8 .. 31; end record; for Program_Status_Word'Size use 8*System.Storage_Unit; for Program_Status_Word'Alignment use 8; More info: https://www.adaic.org/resources/add_content/standards/05aarm/html/AA-13-5-1.html https://www.adaic.org/resources/add_content/standards/05aarm...
- SlowRobotAhead 6y agoI use C for embedded. I do the “lost art” of structure packing all the time. I don’t want ada. I’m not sure why “use a different language” is such a common reply to any language specific discussion.
- samatman 6y agoFor all its somewhat fussy verbosity, Ada really impresses me every time this sort of thing comes up. I keep hoping the Zig developer will do a deep dive on Ada and bring over more of this kind of precise control. A language where I have this kind of control over layout, but can still spell `end record;` as `}`, is ideal for some projects I have in mind.