When you define a struct in C#, the CLR decides how to arrange its fields in memory. This arrangement — the layout — affects both the struct's size and its cache performance. Understanding it can help you write data structures that are smaller and faster.
Alignment and Padding
CPUs access memory most efficiently when values are aligned to their natural boundaries. A 4-byte int should sit at an address divisible by 4. An 8-byte double should sit at an address divisible by 8. When fields are not naturally aligned, the CLR inserts padding bytes between them.
public struct BadLayout
{
public byte A; // 1 byte + 3 bytes padding
public int B; // 4 bytes
public byte C; // 1 byte + 3 bytes padding
public int D; // 4 bytes
}
// Total: 16 bytes (only 10 bytes of actual data)
The CLR pads after A to align B to a 4-byte boundary, and pads after C to align D. Six bytes are wasted on padding.
Reordering Fields
By rearranging fields so that larger types come first, you can eliminate padding:
public struct GoodLayout
{
public int B; // 4 bytes
public int D; // 4 bytes
public byte A; // 1 byte
public byte C; // 1 byte + 2 bytes trailing padding
}
// Total: 12 bytes
That saves 4 bytes per instance. For an array of a million structs, this saves 4 MB.
Checking Struct Size
Use Unsafe.SizeOf<T>() or Marshal.SizeOf<T>() to check sizes:
using System.Runtime.CompilerServices;
Console.WriteLine(Unsafe.SizeOf<BadLayout>()); // 16
Console.WriteLine(Unsafe.SizeOf<GoodLayout>()); // 12
Note: Unsafe.SizeOf<T>() gives the managed size (including internal padding). Marshal.SizeOf<T>() gives the unmanaged/interop size, which may differ.
LayoutKind: Auto, Sequential, and Explicit
The [StructLayout] attribute controls field ordering:
LayoutKind.Sequential (default for structs) — fields appear in memory in the order you declare them. This is required for P/Invoke and interop scenarios.
LayoutKind.Auto — the CLR is free to reorder fields for optimal packing. This is the default for classes.
[StructLayout(LayoutKind.Auto)]
public struct AutoLayout
{
public byte A;
public int B;
public byte C;
public int D;
}
// CLR may reorder to: B, D, A, C — total 12 bytes
LayoutKind.Explicit — you specify the exact byte offset of each field. This is useful for creating unions or overlapping fields:
[StructLayout(LayoutKind.Explicit)]
public struct IntOrFloat
{
[FieldOffset(0)] public int IntValue;
[FieldOffset(0)] public float FloatValue;
}
// IntValue and FloatValue occupy the same 4 bytes
Cache Line Considerations
Modern CPUs load memory in cache lines, typically 64 bytes. When you iterate over an array of structs, smaller structs mean more instances fit in a single cache line, resulting in fewer cache misses.
// 64-byte cache line fits:
// - 4 instances of a 16-byte struct
// - 5 instances of a 12-byte struct
// That's 25% more data per cache line
For large arrays that you iterate frequently, the difference in cache utilisation is measurable.
Practical Example: Particle System
Consider a particle system with millions of particles:
// Poorly packed: 32 bytes
public struct ParticleBad
{
public byte Active; // 1 + 3 padding
public float X; // 4
public byte Type; // 1 + 3 padding
public float Y; // 4
public byte Alpha; // 1 + 7 padding
public double Lifetime; // 8
}
// Well packed: 24 bytes
[StructLayout(LayoutKind.Auto)]
public struct ParticleGood
{
public double Lifetime; // 8
public float X; // 4
public float Y; // 4
public byte Active; // 1
public byte Type; // 1
public byte Alpha; // 1 + 5 trailing padding
}
For one million particles, the poorly packed version uses 32 MB; the well-packed version uses 24 MB. But the real benefit is cache performance — more particles fit in cache during iteration, yielding measurably faster update loops.
The General Rule
Sort fields by size, largest first. This naturally minimises internal padding. For structs used in interop, you may need Sequential layout with explicit packing:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedData
{
public byte A;
public int B;
public byte C;
}
// Total: 6 bytes, no padding — but unaligned access may be slower
Pack = 1 eliminates all padding but forces unaligned memory access, which can be slower on some architectures. Use it only when matching a specific binary format or protocol.
Summary
Struct layout affects both memory usage and cache performance. The default Sequential layout preserves field order, which may introduce padding. Reordering fields by size (largest first) eliminates most padding. For performance-critical structs that do not need interop compatibility, LayoutKind.Auto lets the CLR optimise the layout for you. Always verify sizes with Unsafe.SizeOf<T>() after making changes.