bdmt
bdmt

Reputation: 21

Struct is padded to 8 bytes when 6 bytes seem sufficient

In the write-up The Lost Art of Structure Packing, the author introduces struct foo6 (...) in chapter 6 :

struct foo6 {
    short s;
    char c;
    int flip:1;
    int nybble:4;
    int septet:7;
};

They explain that the padding is done as followed :

struct foo6 {
    short s;       /* 2 bytes */
    char c;        /* 1 byte */
    int flip:1;    /* total 1 bit */
    int nybble:4;  /* total 5 bits */
    int pad1:3;    /* pad to an 8-bit boundary */
    int septet:7;  /* 7 bits */
    int pad2:25;   /* pad to 32 bits */
};

But I don't get why the last bit field is padded to 32 bits. Following the previous explanations, I would have guessed a padding to 16 bits, because the strictest alignment condition is 2 bytes, for the short element. By padding 9 bits to the septet, the struct can be properly aligned on even addresses.

I have a feeling it has to do with the fact that it is a 32 bits system (as in sizeof int == 4) but it is not consistent with the previous explanations.

I have compiled printed the sizeof (struct foo6) with -m32 and -m64, and the result is indeed 8, not 6. But I still don't understand why.

Upvotes: 2

Views: 349

Answers (2)

Luis Colorado
Luis Colorado

Reputation: 12708

Structs always need to be padded to fill the worst case of alignment of any of the fields used in the structure. This is what enforces the padding at the end of the structure, and can make one to think that some bytes are wasted at the end. The reason of this is alignment.

Imagine that a structure makes padding of fields to make one of the fields to be aligned to a 32bit boundary, and finally the struct itself requires 27 bytes. If you are to use it in an array, and don't pad it with one extra byte, the first element of the array will be aligned, but the second will be put at an address that is congruent to 3 mod 4, and every multiple of 4 bytes field will be misaligned one byte of the structure. And for this reason the compiler will add an extra byte at the end of the structure for padding.

Once said this, the alignment of any composed data type, will be the same as the one used in the substructure member with the biggest alignment requirements. In the structure fields, the padding is required to align the next field correctly, and at the end, the padding is required to make the full data type aligned with the next entry in an array of the data type you are defining. In this way you can compute padding and alignment for any compound data type, to be able to join this new created data type with any other in a new data structure, or an array of this new data type.

To compute this, the compiler saves in its type table two sizes, the element size, and the type alignment. These quantities don't have to be equal, for example on a 32 bit machine a short is 2 bytes width, and it will normally have a 2 byte address alignment requirement, but it is possible that it can be accessed in one bus access if positioned at an address that is congruent to 1 mod 4, because it will fit inside a full word, and so have no penalty to be read in a single bus access (while this complicates calculations a lot, I will not enter on this case which is seldom used) So the algorithm normally requires to check the computed offset of a field, and on adding its size, how the alignment is to fit the next field, and add the needed padding bytes to enforce the alignment of the next field. So in the first pass, we will start computing the sizes and alignment requirements of all the fields in the list of fields, grouping the bitfields into the base type chunks (so the int size will be needed to adapt the bitfields into separate padding groups) This way, we have:

  • char fields: These have size 1 and no alignment (consider alignment 1, so they can be aligned at any address)
  • short fields: These have size 2 and alignment to even addresses (2).
  • int fields: We'll consider a 32bit machine so they have 4 size and 4 alignment.
  • long fields: 8 size, and 4 alignment (in a 32 bit machine, 8 alignment in a 64bit)
  • float fields: 4 size and 4 alignment
  • double fields: 8 size and 4 alignment.

Now assume we try to pad the struct foo1 data type:

  1. We start at 0 size, 0 alignment.
  2. associate the size and alignment corresponding to the field we are adding.
  3. If the offset left for this field doesn't correspond to the alignment, add as many bytes as necessary to match the alignment.
  4. compute the new size of the struct and update the alignment if the alignment of this field is larger than the previously calculated.
  5. Repeat from 2 until all fields are considered.
  6. pad the structure as necessary to make its size a multiple of the alignment (round up to the next multiple of the so computed alignment) This requirement allows this data type to be piled up to make an array.

If we apply this to struct foo1 it will result in:

struct foo6 {
    short s;      /* two bytes size, two bytes alignment */
    char c;       /* one byte size, one byte alignment */
    int flip:1;   /* next three fields fit in one 32bit int so
                   * 4 bytes size, 4 bytes alignment. */
    int nybble:4;
    int septet:7;
};
  • s field will be at offset 0, and will set size to 2, alignment to 2
  • c field will be at offset 2, and will set size to 3, alignment to 2
  • flip, nybble and septet have an alignment requirement of 4, so as offset is 3, one extra byte of padding is needed to make offset a multiple of 4 this padding is added before this field. offset is 4, alignment is 4.
  • offset is 4 and alignment and it's a multiple of alignment, so no padding necessary at end, we are finished, final struct foo1 size: 8, alignment: 4.

In the case of struct foo2:

struct foo6 {
    short s;       /* 2 bytes, alignment 2 */
    char c;        /* 1 byte, alignment 1 */
    int flip:1;    /* group 1: flip, nybble and septet */
    int nybble:4;  /* total 12 */
    int septet:7;  /* size 4, alignment 4 */
    int pad2:25;   /* size 4, alignment 4 */
};
  • field s: offset 0, size 2, alignment 2, total size 2, total alignment 2.
  • field c: offset 2, size 1, alignment 1, total size 3, total alignment 2.
  • field group 1: offset 4 (to match alignment of this field, introducing a pad of 1 before it), size 4, alignment 4, total size 8, total alignment 4.
  • field group 2: offset 12, alignment 4, no padding, total size 16, total alignment 4.
  • as total size (16) is a multiple of alignment (4) no extra padding is needed at the end to make the data alignable with siblings in an array.

Upvotes: 0

0___________
0___________

Reputation: 67835

struct foo6 {
    short s;
    char c;
    int flip:1;
    int nybble:4;
    int septet:7;
};

struct foo7 {
    short s;       /* 2 bytes */
    char c;        /* 1 byte */
    int flip:1;    /* total 1 bit */
    int nybble:4;  /* total 5 bits */
    int pad1:3;    /* pad to an 8-bit boundary */
    int septet:7;  /* 7 bits */
    int pad2:25;   /* pad to 32 bits */
};

I have compiled printed the sizeof (struct foo6) with -m32 and -m64, and the result is indeed 8, not 6. But I still don't understand why.

First of all - it is hard to understand which struct as you use the same names to make it unclear. Assuming not packing

  1. foo6
short      [][]
char       []
padding    []
bitfields  [][][]
padding    []

total 8 bytes

  1. foo7
short      [][]
char       []
bitfields  []   <-- GCC will pack them into char 
bitfields  [][][][]

total 8 bytes

Upvotes: 1

Related Questions