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.

Example.cs
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:

Example.cs
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:

Example.cs
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.

Example.cs
[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:

Example.cs
[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.

Example.cs
// 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:

Example.cs
// 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:

Example.cs
[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.