All the fields are aligned depending on their type. The native types (int
, byte
, etc.) are all aligned by their size. For example, an int
will always be at a multiple of 4 bytes in, while a byte can be anywhere.
If smaller fields come before an int
, padding will be added if necessary to ensure the int
is properly aligned to 4 bytes. This is why S5
(1+1+4 = 8) and S8
(1+2+4 = 8) will have padding and end up the same size:
[1][1][ ][ ][4] // S5
[1][ ][ 2 ][4] // S8
Additionally, the struct itself inherits the alignment of its most-aligned field (ie. for S5
and S8
, int
is the most-aligned field, so both of them have an alignment of 4). Alignment is inherited like this so that when you have an array of structs, all the fields in all the structs will be properly aligned. So, 4+2 = 8.
[4][2][ ][ ] // starts at 0
[4][2][ ][ ] // starts at 8
[4][2][ ][ ] // starts at 16
Notice the 4 is always aligned by 4. Without inheriting from the most-aligned field, every other element in an array would have its int
aligned by 6 bytes instead of 4:
[4][2] // starts at 0
[4][2] // starts at 6 -- the [4] is not properly aligned!
[4][2] // starts at 12
This would be very bad because not all architectures allow reading from unaligned memory addresses, and even the ones that do have a (potentially quite large, if on a cache line or page boundary) performance penalty for doing it.
Beyond basic performance, alignment also comes into play with concurrency. The C# memory model guarantees reads/writes of the native types up to 4 bytes wide are atomic, and .NET has atomic features like the Interlocked
class. Atomic operations like these boil down to CPU instructions that themselves require aligned memory access to work.
Proper alignment is very important!
You will often see clever native coders keep all of this in mind while laying out their structures, sorting all fields from largest to smallest in an effort to keep padding, and thus struct size, to a minimum.