Search code examples
c++bit-fieldsstructure-packing

Reordering bit-fields mysteriously changes size of struct


For some reason I have a struct that needs to keep track of 56 bits of information ordered as 4 packs of 12 bits and 2 packs of 4 bits. This comes out to 7 bytes of information total.

I tried a bit field like so

struct foo {
    uint16_t R : 12;
    uint16_t G : 12;
    uint16_t B : 12;
    uint16_t A : 12;
    uint8_t  X : 4;
    uint8_t  Y : 4;
};

and was surprised to see sizeof(foo) evaluate to 10 on my machine (a linux x86_64 box) with g++ version 12.1. I tried reordering the fields like so

struct foo2 {
    uint8_t  X : 4;
    uint16_t R : 12;
    uint16_t G : 12;
    uint16_t B : 12;
    uint16_t A : 12;
    uint8_t  Y : 4;
};

and was surprised that the size now 8 bytes, which is what I originally expected. It's the same size as the structure I expected the first solution to effectively produce:

struct baseline {
    uint16_t first;
    uint16_t second;
    uint16_t third;
    uint8_t  single;
};

I am aware of size and alignment and structure packing, but I am really stumped as to why the first ordering adds 2 extra bytes. There is no reason to add more than one byte of padding since the 56 bits I requested can be contained exactly by 7 bytes.

Minimal Working Example Try it on Wandbox

What am I missing?

PS: none of this changes if we change uint8_t to uint16_t


Solution

  • If we create an instance of struct foo, zero it out, set all bits in a field, and print the bytes, and do this for each field, we see the following:

    R: ff 0f 00 00 00 00 00 00 00 00 
    G: 00 00 ff 0f 00 00 00 00 00 00 
    B: 00 00 00 00 ff 0f 00 00 00 00 
    A: 00 00 00 00 00 00 ff 0f 00 00 
    X: 00 00 00 00 00 00 00 f0 00 00 
    Y: 00 00 00 00 00 00 00 00 0f 00 
    

    So what appears to be happening is that each 12 bit field is starting in a new 16 bit storage unit. Then the first 4 bit field fills out the remaining bits in the prior 16 bit unit, then the last field takes up 4 bits in the last unit. This occupies 9 bites And since the largest field, in this case a bitfield storage unit, is 2 bytes wide, one byte of padding is added at the end.

    So it appears that is 12 bit field, which has a 16 bit base type, is kept within a single 16 bit storage unit instead of being split between multiple storage units.

    If we do the same for the modified struct:

    X: 0f 00 00 00 00 00 00 00 
    R: f0 ff 00 00 00 00 00 00 
    G: 00 00 ff 0f 00 00 00 00 
    B: 00 00 00 00 ff 0f 00 00 
    A: 00 00 00 00 00 00 ff 0f 
    Y: 00 00 00 00 00 00 00 f0 
    

    We see that X takes up 4 bits of the first 16 bit storage unit, then R takes up the remaining 12 bits. The rest of the fields fill out as before. This results in 8 bytes being used, and so requires no additional padding.

    While the exact details of the ordering of bitfields is implementation defined, the C standard does set a few rules.

    From section 6.7.2.1p11:

    An implementation may allocate any addressable storage unit large enough to hold a bit- field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.

    And 6.7.2.1p15:

    Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared.