Rylik

Alignment, Data, and Memory

Like some of the previous topics, alignment has to do with how the CPU works and is less about C, but the examples are, of course, still in C.

Alignment is a very basic concept. If you think of an 8x8x8 box that you have to put things in, and all you have is boxes of those sizes, that’s what alignment is in principle. If you try to fit something 4 units wide in an 8-unit-wide box, you will have 4 units left. It doesn’t matter if you use it or not, that space is there.

There are a couple of things that have alignment. Structs have alignment, primitive types have alignment, memory has alignment and even functions have alignment (very uncommon that you’d ever need to care about function alignment).

Primitive types have a natural alignment equal to their size (NOTE: keep in mind that the context in my articles is x86-64 architecture). For example:

  • char has 1 byte
  • int has 4 bytes
  • float has 4 bytes

All alignments of primitive types are powers of 2.

Alignment for structs works a little differently, since structs can have different types. For example:

struct mytype {
    int a;
    char b;
};

Here, mytype’s alignment is 4, because int has the largest alignment requirement. Whichever field has the largest alignment requirement determines the alignment of the struct.

The following structs all have 4-byte alignment and a size of 8 bytes:

struct a {
   int x;
   char q;
   char w;
};

struct b {
   int x;
   char q;
   char w;
   char e;
};

struct c {
   int x;
   char q;
   char w;
   char e;
   char r;
};

struct af {
    float y;
    int z;
};

struct cf {
   float x;
   char q;
   char w;
   char e;
   char r;
};

So, what happens with the “rest of the space”, when the size of the types doesn’t equal the size of the struct? That’s what is called padding. Taking the box example again, that empty space is essentially the padding. The position of fields in structs also affect the size. For example:

struct mytype {
    float x;
    char a;
    char b;
    char c;
    float y;
    char d;
};
struct mytypesizeof 16 · _Alignof 4
xabc·ydpad

x86-64 System V. Each cell is one byte. Hatched bytes are padding.

Field offsets and sizes
FieldOffsetSizePad before
x040
a410
b510
c610
y841
d1210
Tail padding133—

The alignment here is 4 ( float is the largest and it has a size of 4 bytes) while the size of this struct is 16 bytes, even though two floats + four chars is 12 bytes. (float is 4 so 4 * 2 =8 and a char is 1 byte, so 4 of them is 4 bytes, for a total of 8 + 4=12 ). The reason for that is, again, padding. I will rewrite the struct with field names for where the padding would go:

struct mytype {
    float x;
    char a;
    char b;
    char c;
    char _pad0;
    float y;
    char d;
    char _pad1;
    char _pad2;
    char _pad3;
};

This is also very important for pointer arithmetic, because incrementing the pointer depends on the size of the type it is pointing at.

You can cut down the size by reordering the fields to reduce padding. All of these have 12 bytes (down from 16)

struct mytype0 {
    float x;
    char a;
    char b;
    char c;
    char d;
    float y;

};

struct mytype1 {
    char a;
    char b;
    char c;
    char d;
    float x;
    float y;
};

struct mytype2 {
    float x;
    float y;
    char a;
    char b;
    char c;
    char d;
};
struct mytype0sizeof 12 · _Alignof 4
xabcdy

x86-64 System V. Each cell is one byte. Hatched bytes are padding.

Field offsets and sizes
FieldOffsetSizePad before
x040
a410
b510
c610
d710
y840
Tail padding120—
struct mytype1sizeof 12 · _Alignof 4
abcdxy

x86-64 System V. Each cell is one byte. Hatched bytes are padding.

Field offsets and sizes
FieldOffsetSizePad before
a010
b110
c210
d310
x440
y840
Tail padding120—
struct mytype2sizeof 12 · _Alignof 4
xyabcd

x86-64 System V. Each cell is one byte. Hatched bytes are padding.

Field offsets and sizes
FieldOffsetSizePad before
x040
y440
a810
b910
c1010
d1110
Tail padding120—

In C, to view the size or alignment of types, you can use the following:

#include <stddef.h>
struct mytype2 {
    float x;
    float y;
    char a;
    char b;
    char c;
    char d;
};

static size_t size = sizeof(struct mytype2);
static size_t alignment = _Alignof(struct mytype2);

Although some compilers might have __alignof__ for getting alignment as well. It is also worth briefly mentioning the "offsetof" because it is related to alignment.

#include <stddef.h>
struct mytype2 {
    float x; /*offset 0  with size 4*/
    float y; /*offset 4  with size 4*/
    char a;  /*offset 8  with size 1*/
    char b;  /*offset 9  with size 1*/
    char c;  /*offset 10 with size 1*/
    char d;  /*offset 11 with size 1*/
};

static size_t offset_a = offsetof(struct mytype2, a); /*result is 8*/

Watch what happens with the offset in this case:

struct mytype {

    float x; /*offset 0 with size 4*/

    char a;  /*offset 4 with size 1*/

    char b;  /*offset 5 with size 1*/

    char c;  /*offset 6 with size 1*/

/*The next usable byte after c is byte 7, but float y needs 4-byte alignment, so byte 7 becomes padding and y starts at offset 8.*/

    float y; /*offset 8 with size 4*/

    char d;  /*offset 12 with size 1*/

};

Notice that 1-byte types do not need padding before them, they can be put after any type and the offset increments by exactly 1 for them. In the example above, the field "y" has offset 8 with a size of 4 and "8 + 4 = 12" is exactly the offset of the field "d", because the required alignment is 1 for 1-byte types.

This will conclude alignment. It will probably be the most important thing continuing in this series, so make sure you understand it well.

Found this useful? Support the writing on Ko-fi.