📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is memory alignment and padding? How does the CLR lay out structs and classes in memory, and when would you control layout with StructLayout?

Natural alignment, padding and why misaligned access is slow or non-atomic, how the CLR lays out classes (auto) and structs (sequential by default, auto when they contain references), object headers and minimum object size, measuring sizes, and using StructLayout, FieldOffset, Pack and InlineArray for interop and performance.

Senior Theory Advanced interop memory memory-alignment performance struct-layout

🎯 Key points

  • Natural alignment: a value of size N is stored at an address that is a multiple of N (up to the platform word size); a struct's alignment is its largest field's alignment
  • Padding is inserted between fields and at the end of a struct (rounding the size up to its alignment) so fields and array elements stay aligned
  • Misaligned access can be slower, can straddle two cache lines, and is not guaranteed atomic; Interlocked operations require alignment
  • CLR classes use auto layout (the runtime reorders fields: references first, then larger to smaller); C# structs are emitted as sequential, but the runtime uses auto layout in managed memory for structs that contain references, and sequential then only governs the marshalled copy
  • On 64-bit every object has an 8-byte header and an 8-byte method table pointer, and the minimum object size is 24 bytes (the GC needs at least one pointer-sized field slot), so a class holding one int costs 24 bytes plus the reference to it
  • Unsafe.SizeOf<T> gives the managed size; Marshal.SizeOf<T> gives the marshalled (native) size, which can differ (bool is 1 byte managed, 4 bytes marshalled by default)
  • Use StructLayout (Sequential with Pack, Explicit with FieldOffset) and InlineArray for interop, binary formats and fixed-size buffers; otherwise let the runtime choose and just order struct fields largest to smallest when size matters

⏱ 30-second answer

CPUs read memory most efficiently when a value sits at an address that is a multiple of its size, which is called natural alignment: a 4-byte int at a multiple of 4, an 8-byte long at a multiple of 8. Compilers and the CLR add padding between fields to keep that alignment, so struct { byte; long; byte; } takes 24 bytes, not 10. Misaligned access can be slower, can span two cache lines, and isn't guaranteed atomic. In .NET, classes use auto layout (the runtime reorders fields to minimise padding), structs default to sequential layout (but get auto layout if they contain references), and every object carries a 16-byte overhead on 64-bit. I use StructLayout, FieldOffset and Pack when matching a native or binary layout, and otherwise let the runtime decide.

🗣 Full answer (senior)

Why alignment exists

  • The memory bus and caches move aligned chunks. An aligned 8-byte value is always inside one 64-byte cache line; a misaligned one can straddle two, needing two accesses.
  • Some architectures historically faulted on misaligned access; x64 and Arm64 tolerate most cases but may be slower, and atomic instructions (Interlocked) require alignment.
  • Reads and writes of naturally aligned values up to the pointer size are atomic in .NET (no torn reads). A misaligned long (possible only with explicit layouts or unsafe code) can tear.

Padding in action

struct Bad  { byte A; long B; byte C; }      struct Good { long B; byte A; byte C; }
offset: 0 A | 1-7 pad | 8-15 B | 16 C | 17-23 pad      0-7 B | 8 A | 9 C | 10-15 pad
size:  24 bytes                                        size: 16 bytes

The size of a struct is rounded up to a multiple of its largest field's alignment so that arrays of it stay aligned.

How the CLR lays things out

Type Default layout Notes
Class Auto Fields reordered (references first, then larger to smaller primitives) to reduce padding; you can't rely on order
Struct with only unmanaged fields Sequential C# compiler emits LayoutKind.Sequential; fields stay in declaration order with padding
Struct containing references Auto in managed memory The runtime reorders for the GC; sequential only applies when marshalled
[StructLayout(LayoutKind.Explicit)] You choose each FieldOffset Used for interop and unions; overlapping references with values is not allowed

Object overhead on 64-bit: an object header (8 bytes, used for locking and hashing) plus a method table pointer (8 bytes), and a minimum object size of 24 bytes. A class with one int field uses 24 bytes; the same data in a struct array uses 4 bytes per element.

Measuring sizes

  • Unsafe.SizeOf<T>(): managed size of a value type (including padding).
  • Marshal.SizeOf<T>(): size when marshalled to native code, which can differ (for example bool is 4 bytes by default when marshalled, 1 byte managed).
  • sizeof(T) for unmanaged types (in an unsafe context for user-defined structs).
  • GC.GetAllocatedBytesForCurrentThread() around an allocation, or a memory profiler, for class instances.

When to control layout

  • Interop: matching a C struct passed to a native library (LibraryImport), using Sequential with the right Pack, or Explicit offsets.
  • Binary formats: reading a fixed header with MemoryMarshal.Read<T> (mind endianness too).
  • Fixed-size buffers: [InlineArray(N)] (.NET 8+) gives a struct with N inline elements, safer than fixed buffers.
  • Performance: reorder struct fields from largest to smallest to cut padding in very large arrays; pad hot fields onto separate cache lines to avoid false sharing.

Production example

A market-data service kept 50 million quote structs in memory. The struct was declared as bool IsBid; double Price; int Size; bool IsFirm;, which padded to 24 bytes. Reordering to double Price; int Size; bool IsBid; bool IsFirm; made it 16 bytes, saving about 400 MB and improving scan speed because more quotes fitted per cache line.

🔍 What the interviewer is checking

Checks low-level memory knowledge relevant to interop and high-scale data handling.

Green flags: explains natural alignment and padding with an example, knows auto versus sequential layout and object overhead, distinguishes Unsafe.SizeOf from Marshal.SizeOf, uses StructLayout appropriately, mentions atomicity.

Red flags: thinks size equals the sum of fields; uses Pack = 1 as a general optimisation; doesn't know classes have per-object overhead.

📚 Learn in depth

1. Alignment rules (64-bit)

Type Size Natural alignment
byte, bool 1 1
short, char 2 2
int, float 4 4
long, double, references, nint 8 8
decimal 16 8
Vector128<T> 16 16 (best performance; unaligned loads allowed)

Struct alignment = largest field alignment; struct size = padded to a multiple of that.

2. What an object looks like

          reference points here
                 |
                 v
[ object header (8) ][ MethodTable* (8) ][ fields ... (auto layout) ][pad]
  sync block index,     type info,          references grouped first
  hash code, lock bits  vtable, GC info     then 8, 4, 2, 1-byte fields

int[] array:  [header][MethodTable*][length (8 on 64-bit, incl. padding)][elements...]

3. Pack and Explicit

[StructLayout(LayoutKind.Sequential, Pack = 1)]   // no padding: matches a packed C struct
public struct PackedHeader { public byte Version; public uint Length; public ushort Flags; }   // 7 bytes

[StructLayout(LayoutKind.Explicit)]
public struct Int32Bytes                          // a union: same 4 bytes viewed two ways
{
    [FieldOffset(0)] public int Value;
    [FieldOffset(0)] public byte B0;
    [FieldOffset(1)] public byte B1;
    [FieldOffset(2)] public byte B2;
    [FieldOffset(3)] public byte B3;
}

Pack = 1 makes Length misaligned at offset 1, which is fine for marshalling but slower to access and not atomic.

4. Managed versus marshalled layout

Field Managed Default marshalled
bool 1 byte 4 bytes (Win32 BOOL) unless [MarshalAs(UnmanagedType.U1)]
char 2 bytes 1 or 2 depending on CharSet
string reference pointer to a native string, copied

With LibraryImport (source-generated interop), prefer blittable types (only primitives and blittable structs) so data passes without copying. DisableRuntimeMarshalling makes that explicit.

5. Aligned native memory

NativeMemory.AlignedAlloc(size, alignment) and NativeMemory.AlignedFree (.NET 6+) allocate memory aligned to, for example, 64 bytes for SIMD or cache-line-sized buffers.

6. Inspecting layout

  • Unsafe.SizeOf<T>() and field offsets via a quick unsafe helper or ObjectLayoutInspector (a community NuGet package that prints field offsets and padding).
  • Visual Studio's Memory window shows the bytes of an object in a debugging session.

7. Try it yourself

  1. Declare Bad and Good from the answer and print Unsafe.SizeOf for each; then allocate 10 million of each in arrays and compare GC.GetTotalMemory.
  2. Add a string field to Bad and check whether the size and order change.
  3. Compare Unsafe.SizeOf<T>() and Marshal.SizeOf<T>() for a struct with a bool field.
  4. Define an [InlineArray(8)] struct of int and use it as a fixed-size buffer with Span<int>.

💻 Code

namespace Alignment;

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public struct Bad  { public bool IsBid; public double Price; public int Size; public bool IsFirm; }
public struct Good { public double Price; public int Size; public bool IsBid; public bool IsFirm; }

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedHeader { public byte Version; public uint Length; public ushort Flags; }

public struct WithBool { public bool Flag; public int Value; }

[InlineArray(8)]
public struct Buffer8 { private int _element0; }

public static class Program
{
    public static void Main()
    {
        Console.WriteLine($"Bad:          {Unsafe.SizeOf<Bad>()} bytes");          // 24
        Console.WriteLine($"Good:         {Unsafe.SizeOf<Good>()} bytes");         // 16
        Console.WriteLine($"PackedHeader: {Unsafe.SizeOf<PackedHeader>()} bytes");  // 7
        Console.WriteLine($"WithBool managed {Unsafe.SizeOf<WithBool>()}, marshalled {Marshal.SizeOf<WithBool>()}"); // 8 vs 8 (bool marshals as 4 bytes, then int)

        long before = GC.GetAllocatedBytesForCurrentThread();
        var bad = new Bad[1_000_000];
        long badBytes = GC.GetAllocatedBytesForCurrentThread() - before;

        before = GC.GetAllocatedBytesForCurrentThread();
        var good = new Good[1_000_000];
        long goodBytes = GC.GetAllocatedBytesForCurrentThread() - before;
        Console.WriteLine($"1M Bad: {badBytes / 1_000_000.0:F1} MB, 1M Good: {goodBytes / 1_000_000.0:F1} MB");

        var buffer = new Buffer8();
        Span<int> span = buffer;                 // InlineArray converts to Span<T>
        span.Fill(7);
        Console.WriteLine(span.Length);          // 8
        GC.KeepAlive(bad); GC.KeepAlive(good);
    }
}

⚠ Common mistakes

  • Mistake: assuming a struct's size is the sum of its fields. Padding for alignment can add a lot: bool, double, int, bool is 24 bytes, not 14. Instead: measure with Unsafe.SizeOf<T>() and order fields from largest to smallest when size matters.

  • Mistake: relying on class field order. The CLR uses auto layout for classes and may reorder fields, so code that assumes offsets (unsafe reads, serialisers that copy memory) breaks. Instead: use explicit, sequential structs for anything that needs a fixed layout.

  • Mistake: using Pack = 1 everywhere "to save memory". Misaligned fields are slower to access, can straddle cache lines, and lose atomicity guarantees. Instead: use packing only to match external layouts, and reorder fields for compactness otherwise.

  • Mistake: confusing managed size with marshalled size. bool is 1 byte managed but 4 bytes when marshalled by default, so native code reads the wrong offsets. Instead: check Marshal.SizeOf, use blittable types with LibraryImport, and add [MarshalAs] where needed.

  • Mistake: forgetting object overhead when sizing caches. A class with one int costs 24 bytes plus the reference that points to it, so millions of tiny objects use far more memory than expected. Instead: estimate per-object overhead in capacity planning, and use struct arrays for very large collections of small records.

  • Mistake: overlapping references with values in an explicit layout. The runtime rejects types that overlay an object reference with other fields, because the GC couldn't tell what's a reference. Instead: keep references out of explicit unions, or use separate fields.

🏛 Senior insight: trade-offs

Alignment is a niche topic that pays off in three places: interop, very large in-memory datasets, and concurrency.

Layout choice Use it when Risk if misused
Default (auto for classes, sequential for unmanaged structs) Normal code None; do not depend on class field order
Reorder struct fields largest to smallest Millions of structs in arrays None; pin the size with a unit test
Sequential, Pack = 1 Matching a packed native or wire format Slower misaligned access, non-atomic reads
Explicit + FieldOffset Native unions, exact offsets Runtime rejects overlapping references; easy to get offsets wrong
[InlineArray(N)] Fixed-size buffers inside structs Size fixed at compile time
Padding to 64/128 bytes Hot per-thread data Wasted memory if applied broadly

Seniors know the CLR's defaults and why they exist, measure sizes instead of guessing, and keep explicit layout confined to boundary types (interop and serialisation) so the rest of the codebase stays idiomatic. They also connect it to capacity planning: memory per item, items per cache line and the 16-byte object overhead are real numbers when sizing caches and container memory limits. Security note: reading untrusted bytes straight into a struct with MemoryMarshal.Read<T> skips validation, so lengths and enum values from that struct must be checked before use.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions padding hot fields onto separate cache lines. Why: Checks the concurrency side of layout: padding against false sharing.
    How to answer

    Cores keep their caches coherent: when one core writes to a cache line, copies of that whole 64-byte line in other cores are invalidated. If thread A updates counters[0] and thread B updates counters[1], they're logically independent but sit on the same line, so the line ping-pongs between cores on every write. That's false sharing: more threads, more invalidations, and the loop gets slower. Symptoms are high CPU with poor scaling and no locks in sight. The best fix is to accumulate in a local variable per thread and combine once at the end (Parallel.For with localInit/localFinally). Where per-thread state must live in shared memory, pad each slot to its own cache line (128 bytes is safe across x64 and Apple Arm64).

  2. When: Candidate uses explicit layouts to read binary formats. Why: Checks the other half of binary layout compatibility: byte order.
    How to answer

    Endianness is the order in which the bytes of a multi-byte value are stored. Little-endian puts the least significant byte first, so 0x12345678 is stored as 78 56 34 12. Big-endian puts the most significant byte first: 12 34 56 78. x64 and Arm64 machines running .NET are little-endian, but network protocols ("network byte order") and many file formats are big-endian. In .NET I use BinaryPrimitives.ReadInt32BigEndian and friends so the byte order is explicit and portable. BitConverter follows the machine, and BinaryReader/BinaryWriter are always little-endian.

🛠 Real-world problems: how you handle them

Saving 400 MB in a market-data cache by reordering struct fields
Situation
A market-data service held about 50 million quote structs in arrays in memory on 8 GB pods in AKS, and pods were close to their memory limit.
Signals
Container memory near the limit, occasional OOMKilled restarts at market open, and a heap profile showing the quote arrays as the largest consumer.
Investigation
Measured `Unsafe.SizeOf<Quote>()` (24 bytes) against the sum of fields (14 bytes), reviewed field order and confirmed no code depended on the layout.
Resolution
Reordered fields from largest to smallest so the struct became 16 bytes, saving about 400 MB per pod and speeding up scans because more quotes fitted per cache line.
Prevention
Added a unit test asserting the struct's size, a note in the type's documentation, and memory headroom alerts on the pods.

How to tell it (STAR)

**Situation:** At [company], our market-data pods were running close to their [8 GB] memory limit and getting OOM-killed at market open. **Task:** I needed to reduce memory without dropping data. **Action:** The heap profile showed the quote arrays were the biggest consumer. Measuring the struct showed [24] bytes for [14] bytes of fields because of padding. I reordered the fields from largest to smallest, getting it to [16] bytes, and added a test that pins the size. **Result:** Each pod used about [400 MB] less, the OOM restarts stopped, and scans got faster because more quotes fitted in each cache line.

↩ Usually asked after

More in Memory

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me