3

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

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
geo
  • 435
  • 3
  • 13
  • 1
    Note that bit fields other than with `_Bool`, `int`, `signed int`, or `unsigned int` rely on implementation-defined details. – chux - Reinstate Monica Aug 02 '22 at 19:36
  • 2
    I'd guess that the compiler prefers not to break a single multi-bit field across two different storage units (such as G in the first snippet - between the first and second words) – Eugene Sh. Aug 02 '22 at 19:42
  • Isn't it padding a byte between word boundaries in foo and removing the padding in foo2 since 8 bytes of G exist on the first word and eight on the second? – Girspoon Aug 02 '22 at 19:46
  • The way bit fields were implemented they are, in my mind (and IMHO), totally worthless. Build yourself a couple of routines to get and set bits as correctly ordered. For C++, a [std::bitset](https://en.cppreference.com/w/cpp/utility/bitset) is your friend. – Dúthomhas Aug 02 '22 at 19:55

1 Answers1

2

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.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • Which generally means you should be using a storage type large enough to hold the whole bitfield whenever possible. Or at least never have members of the bitfield cross boundaries of the storage type. Note that the storage type used affects alignment of the bitfield. Reading a member that crosses boundaries of the storage type would mean reading 2 values of the used type, shifting, masking and recombining the value. Reverse that for writing. So most compilers don't pack bitfields like that. – Goswin von Brederlow Aug 03 '22 at 17:43