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;
};
x86-64 System V. Each cell is one byte. Hatched bytes are padding.
Field offsets and sizes
| Field | Offset | Size | Pad before |
|---|---|---|---|
| x | 0 | 4 | 0 |
| a | 4 | 1 | 0 |
| b | 5 | 1 | 0 |
| c | 6 | 1 | 0 |
| y | 8 | 4 | 1 |
| d | 12 | 1 | 0 |
| Tail padding | 13 | 3 | — |
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;
};
x86-64 System V. Each cell is one byte. Hatched bytes are padding.
Field offsets and sizes
| Field | Offset | Size | Pad before |
|---|---|---|---|
| x | 0 | 4 | 0 |
| a | 4 | 1 | 0 |
| b | 5 | 1 | 0 |
| c | 6 | 1 | 0 |
| d | 7 | 1 | 0 |
| y | 8 | 4 | 0 |
| Tail padding | 12 | 0 | — |
x86-64 System V. Each cell is one byte. Hatched bytes are padding.
Field offsets and sizes
| Field | Offset | Size | Pad before |
|---|---|---|---|
| a | 0 | 1 | 0 |
| b | 1 | 1 | 0 |
| c | 2 | 1 | 0 |
| d | 3 | 1 | 0 |
| x | 4 | 4 | 0 |
| y | 8 | 4 | 0 |
| Tail padding | 12 | 0 | — |
x86-64 System V. Each cell is one byte. Hatched bytes are padding.
Field offsets and sizes
| Field | Offset | Size | Pad before |
|---|---|---|---|
| x | 0 | 4 | 0 |
| y | 4 | 4 | 0 |
| a | 8 | 1 | 0 |
| b | 9 | 1 | 0 |
| c | 10 | 1 | 0 |
| d | 11 | 1 | 0 |
| Tail padding | 12 | 0 | — |
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.