You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Stack vs Heap Allocation

Learn where different types are stored and how memory is managed

Stack vs Heap Allocation in C#

Understanding stack and heap memory allocation is fundamental to mastering C# performance optimization and memory management. Master these concepts with free flashcards and spaced repetition to reinforce your learning. This lesson covers the differences between stack and heap memory, allocation mechanisms, value vs reference type storage, and performance implicationsβ€”essential knowledge for building efficient C# applications.

Welcome πŸ’»

Every time your C# program creates a variable, stores an object, or calls a method, the runtime must decide where to store that data in memory. This decision happens behind the scenes, but understanding it transforms you from someone who writes code that "just works" to someone who writes efficient, predictable, high-performance applications.

The stack and heap are two distinct memory regions with fundamentally different characteristics. The stack operates like a stack of platesβ€”fast, organized, and automatic. The heap resembles a vast warehouse where items can be stored anywhere, requiring more management but offering greater flexibility. Knowing when and why C# uses each storage location gives you superpowers for debugging memory issues, optimizing performance, and understanding how your code really executes.

Core Concepts 🧠

What Are Stack and Heap?

The Stack is a contiguous block of memory with a Last-In-First-Out (LIFO) structure. Think of it like a stack of cafeteria traysβ€”you can only add or remove from the top. When a method executes, the runtime allocates a stack frame containing:

  • Local variables (value types)
  • Method parameters
  • Return addresses
  • References to heap objects

The Heap is a larger, more flexible memory region used for dynamic allocation. It's like a parking lot where you can park anywhere there's space, but you need to remember where you parked (via references). The heap stores:

  • All reference type objects (classes, arrays, delegates)
  • Data that outlives a single method call
  • Objects of unpredictable or large size
MEMORY LAYOUT

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           STACK                     β”‚  ← Fast, automatic
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚     LIFO structure
β”‚  β”‚ Method Frame 3   β”‚               β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€               β”‚
β”‚  β”‚ Method Frame 2   β”‚  Growing ↓    β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€               β”‚
β”‚  β”‚ Method Frame 1   β”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                     β”‚
β”‚           HEAP                      β”‚  ← Flexible, managed
β”‚  ╔═══════════╗  ╔════════╗          β”‚     by Garbage Collector
β”‚  β•‘ Object A  β•‘  β•‘ Array  β•‘          β”‚
β”‚  β•šβ•β•β•β•β•β•β•β•β•β•β•β•  β•šβ•β•β•β•β•β•β•β•β•          β”‚
β”‚        ╔══════════════╗              β”‚
β”‚        β•‘  Object B    β•‘              β”‚
β”‚        β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Stack Allocation: Fast and Automatic ⚑

When you declare a value type variable inside a method, the runtime allocates space on the stack. This operation is blazingly fastβ€”just moving a stack pointer (a simple increment/decrement).

Key characteristics:

  • Allocation speed: O(1) constant time, just pointer adjustment
  • Deallocation: Automatic when method returns (pop the frame)
  • Lifetime: Scoped to the method execution
  • Thread-safe: Each thread has its own stack
  • Size limit: Typically 1MB (can cause StackOverflowException)
  • Organization: Contiguous, cache-friendly memory

πŸ’‘ Memory tip: Stack variables are "born" when execution reaches their declaration and "die" when the scope endsβ€”no cleanup required!

Heap Allocation: Flexible but Managed πŸ“¦

When you use new to create a reference type object, the runtime searches the heap for sufficient space. This involves more overhead but provides crucial benefits.

Key characteristics:

  • Allocation speed: Slower than stack (search for free space)
  • Deallocation: Managed by Garbage Collector (GC)
  • Lifetime: Until no references remain (GC-determined)
  • Shared across threads: Requires synchronization
  • Size limit: Much larger (gigabytes possible)
  • Organization: Fragmented, requires GC compaction

⚠️ Critical point: Every heap allocation creates work for the Garbage Collector. Excessive allocations can trigger frequent GC collections, causing performance hiccups.

What Gets Stored Where? 🎯

Type Category Storage Location Examples Contains
Value Types Stack (usually)* int, double, bool, struct, enum, char Actual data
Reference Types Heap class, string, array, delegate, object Object data
References Stack Variables holding class instances Memory address (pointer)

*Exception: Value types inside reference types are stored on the heap as part of the object.

The Reference-Data Split πŸ”—

This is the most important concept: reference type variables are split between stack and heap.

Person person = new Person { Name = "Alice" };

What happens in memory:

  1. Stack: The variable person stores a reference (memory address)
  2. Heap: The actual Person object with Name = "Alice" lives here

The reference is like a street address written on paper (stack)β€”the actual house is elsewhere (heap).

Memory Allocation Deep Dive πŸ”

Stack Frame Anatomy

Each method call creates a stack frame containing:

public int Calculate(int x, int y)
{
    int sum = x + y;
    int product = x * y;
    return sum + product;
}
STACK FRAME for Calculate(5, 3)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Return Address          β”‚ ← Where to resume after method
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Parameters:             β”‚
β”‚   x = 5                 β”‚
β”‚   y = 3                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Local Variables:        β”‚
β”‚   sum = 8               β”‚
β”‚   product = 15          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Return Value = 23       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↑ Frame popped when method returns

When Calculate returns, the entire frame is popped instantlyβ€”all local variables disappear.

Heap Object Structure

Heap objects contain hidden overhead:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Product product = new Product { Id = 1, Name = "Laptop", Price = 999.99m };
HEAP OBJECT LAYOUT
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Object Header (8-16 bytes)    β”‚ ← Sync block, type info
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Method Table Pointer          β”‚ ← Points to type metadata
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Id: 1 (4 bytes)               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Name: (reference)             β”‚ ← Points to string on heap
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Price: 999.99 (16 bytes)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Total: ~40+ bytes (plus string data)

πŸ’‘ Optimization insight: A simple Product object has significant overhead. Creating millions of these can pressure memory and GC. Consider using struct for small, short-lived data.

Value Types as Class Members

Crucial rule: Value types follow their container!

public struct Point  // Value type
{
    public int X;
    public int Y;
}

public class Shape  // Reference type
{
    public Point Location;  // Where is this stored?
    public int Sides;
}

Shape shape = new Shape { Location = new Point { X = 10, Y = 20 }, Sides = 4 };
MEMORY LAYOUT

STACK:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ shape: 0x2A3F    β”‚ ← Reference to heap
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

HEAP (at address 0x2A3F):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Shape Object            β”‚
β”‚  Location.X: 10         β”‚ ← Value type stored inline
β”‚  Location.Y: 20         β”‚    (not a separate object)
β”‚  Sides: 4               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Point struct is embedded directly in the Shape object on the heapβ€”it's not a separate heap allocation.

Practical Examples πŸ’‘

Example 1: Value Type Behavior

public void DemonstrateValueTypes()
{
    int original = 10;
    int copy = original;  // Copies the VALUE
    copy = 20;
    
    Console.WriteLine($"original: {original}");  // 10
    Console.WriteLine($"copy: {copy}");          // 20
}

Memory analysis:

STACK (after copy = 20):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original: 10     β”‚ ← Unchanged
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ copy: 20         β”‚ ← Modified independently
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Two separate memory locations!

Why it matters: Value types provide copy semantics. Each variable has its own independent storage. Modifying copy doesn't affect original.

Example 2: Reference Type Behavior

public class Account
{
    public decimal Balance { get; set; }
}

public void DemonstrateReferenceTypes()
{
    Account original = new Account { Balance = 1000 };
    Account copy = original;  // Copies the REFERENCE
    copy.Balance = 500;
    
    Console.WriteLine($"original.Balance: {original.Balance}");  // 500 (!)
    Console.WriteLine($"copy.Balance: {copy.Balance}");          // 500
}

Memory analysis:

STACK:                    HEAP:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ original: 0x4B2C β”‚ ──→ β”‚ Account         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”‚  Balance: 500   β”‚
β”‚ copy: 0x4B2C     β”‚ ──→ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            ↑
  Both reference the same object!

Why it matters: Reference types provide reference semantics. Multiple variables can reference the same object. Changes through any reference affect all others.

🧠 Memory device: "Value types hold Values. Reference types hold References (addresses)."

Example 3: Method Parameters and Memory

public struct PointStruct { public int X, Y; }
public class PointClass { public int X, Y; }

public void ModifyStruct(PointStruct point)
{
    point.X = 100;  // Modifies the COPY
}

public void ModifyClass(PointClass point)
{
    point.X = 100;  // Modifies the ORIGINAL
}

public void TestParameterPassing()
{
    PointStruct ps = new PointStruct { X = 10, Y = 20 };
    ModifyStruct(ps);
    Console.WriteLine(ps.X);  // 10 (unchanged)
    
    PointClass pc = new PointClass { X = 10, Y = 20 };
    ModifyClass(pc);
    Console.WriteLine(pc.X);  // 100 (changed!)
}

Memory flow for struct:

1. ps created on stack: { X: 10, Y: 20 }
2. ModifyStruct called β†’ COPIES entire struct to new stack frame
3. Modification affects only the copy
4. Stack frame popped β†’ copy destroyed
5. Original ps unchanged

Memory flow for class:

1. pc reference on stack β†’ points to heap object { X: 10, Y: 20 }
2. ModifyClass called β†’ COPIES reference to new stack frame
3. Both references point to same heap object
4. Modification affects the shared heap object
5. Original pc reference now sees { X: 100, Y: 20 }

πŸ’‘ Performance tip: Passing large structs by value copies all their data. For large value types, use ref or in parameters to pass by reference and avoid copying.

Example 4: Boxing and Heap Allocation

public void DemonstrateBoxing()
{
    int value = 42;              // Stack: value type
    object boxed = value;        // BOXING: copies to heap!
    int unboxed = (int)boxed;    // UNBOXING: copies back to stack
    
    // Performance problem:
    ArrayList list = new ArrayList();
    for (int i = 0; i < 1000; i++)
    {
        list.Add(i);  // Each Add boxes the int β†’ 1000 heap allocations!
    }
    
    // Better approach:
    List<int> genericList = new List<int>();
    for (int i = 0; i < 1000; i++)
    {
        genericList.Add(i);  // No boxing! Stores ints directly
    }
}

Boxing process:

STACK:                    HEAP:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         
β”‚ value: 42    β”‚ ─ copy ─→ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€           β”‚ Boxed Int Object β”‚
β”‚ boxed: 0x7A8 β”‚ ────────→ β”‚  Value: 42       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Boxing creates a heap object wrapper!

⚠️ Performance killer: Boxing converts stack-allocated value types to heap-allocated objects. This triggers:

  1. Heap allocation (slow)
  2. Memory copy
  3. Garbage collector pressure
  4. Cache misses

In tight loops, boxing can destroy performance. Always use generic collections (List<T>, Dictionary<TKey, TValue>) to avoid boxing.

Performance Implications ⚑

Allocation Speed Comparison

Operation Stack Heap Speed Ratio
Allocate single int ~1 nanosecond ~10-50 nanoseconds 10-50x slower
Allocate 1000 ints ~1 microsecond ~10-50 microseconds 10-50x slower
Deallocation Instant (frame pop) Deferred (GC) N/A

Garbage Collection Impact

The Garbage Collector runs when:

  • Gen 0 fills up (~256KB-4MB of short-lived objects)
  • Explicit GC.Collect() called (avoid in production!)
  • Memory pressure detected

GC pause times:

  • Gen 0 collection: 0.5-2 milliseconds
  • Gen 1 collection: 2-10 milliseconds
  • Gen 2 (full) collection: 50-500+ milliseconds

πŸ’‘ Optimization strategy: Reduce heap allocations to minimize GC frequency. Techniques:

  • Use structs for small, short-lived data (<16 bytes)
  • Object pooling for frequently created/destroyed objects
  • Span<T> and Memory<T> for stack-like heap allocations
  • ArrayPool<T> for reusing arrays

Struct vs Class Decision Matrix

Use Struct When... Use Class When...
βœ“ Size ≀ 16 bytes βœ“ Size > 16 bytes
βœ“ Immutable (read-only) βœ“ Mutable state needed
βœ“ Short lifetime βœ“ Long lifetime
βœ“ Rarely boxed βœ“ Polymorphism needed
βœ“ Value semantics (copy) βœ“ Reference semantics (share)
Example: Point, Color, Vector2 Example: Customer, Order, HttpClient

πŸ”¬ Advanced technique: Use readonly struct to ensure immutability and avoid defensive copies:

public readonly struct ImmutablePoint
{
    public int X { get; }
    public int Y { get; }
    
    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Common Mistakes ⚠️

Mistake 1: Large Struct Copying

// BAD: Large struct (96 bytes)
public struct Matrix4x4
{
    public float M11, M12, M13, M14;
    public float M21, M22, M23, M24;
    public float M31, M32, M33, M34;
    public float M41, M42, M43, M44;
}

public Matrix4x4 ProcessMatrix(Matrix4x4 matrix)  // Copies 96 bytes!
{
    // Process matrix...
    return matrix;  // Copies another 96 bytes!
}

// GOOD: Pass by reference
public void ProcessMatrix(in Matrix4x4 matrix)  // Only copies 8-byte reference
{
    // Process matrix (read-only)...
}

Impact: Passing a 96-byte struct by value copies all 96 bytes onto the stack. In a loop called millions of times, this wastes CPU cycles and cache bandwidth.

Mistake 2: Capturing Variables in Closures

public List<Action> CreateActions()
{
    List<Action> actions = new List<Action>();
    
    for (int i = 0; i < 10; i++)
    {
        int local = i;  // Stack variable
        actions.Add(() => Console.WriteLine(local));  // CLOSURE!
    }
    
    return actions;
}

What happens: The compiler transforms this into a class with a field for local. Each captured variable forces a heap allocation:

// Compiler-generated (conceptual):
class Closure
{
    public int local;
    public void Action() => Console.WriteLine(local);
}

Each iteration creates a new Closure object on the heapβ€”10 heap allocations total!

Mistake 3: String Concatenation in Loops

// BAD: Creates many intermediate string objects
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString();  // 1000 heap allocations!
}

// GOOD: Reuses internal buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i);  // Minimal allocations
}
string result = sb.ToString();

Why: Strings are immutable. Each += creates a new string object on the heap, copying all previous characters plus the new ones. With 1000 iterations, this creates ~500,000 intermediate strings!

Mistake 4: Unnecessary LINQ in Hot Paths

// BAD: Creates iterator objects (heap allocations)
for (int frame = 0; frame < 60; frame++)  // Game loop
{
    var activeEnemies = enemies.Where(e => e.IsActive).ToList();  // Allocates!
    ProcessEnemies(activeEnemies);
}

// GOOD: Manual filtering with reused list
List<Enemy> activeEnemies = new List<Enemy>(enemies.Count);
for (int frame = 0; frame < 60; frame++)
{
    activeEnemies.Clear();  // Reuse capacity
    foreach (var enemy in enemies)
    {
        if (enemy.IsActive)
            activeEnemies.Add(enemy);
    }
    ProcessEnemies(activeEnemies);
}

Impact: LINQ creates iterator objects and often allocates result collections. In performance-critical code (60 FPS game loops), this triggers frequent garbage collection.

Mistake 5: Mutable Structs

// BAD: Mutable struct causes confusion
public struct MutablePoint
{
    public int X { get; set; }
    public int Y { get; set; }
    
    public void MoveRight() => X++;  // Modifies copy!
}

var point = new MutablePoint { X = 10, Y = 20 };
point.MoveRight();
Console.WriteLine(point.X);  // 11 (expected)

// But what about this?
var points = new List<MutablePoint> { point };
points[0].MoveRight();  // Modifies copy, not the stored struct!
Console.WriteLine(points[0].X);  // Still 11 (not 12!)

Why: points[0] returns a copy of the struct. You're modifying the copy, not the original in the list. This is confusing and error-prone.

Solution: Make structs immutable (readonly struct) and return modified copies:

public readonly struct ImmutablePoint
{
    public int X { get; }
    public int Y { get; }
    
    public ImmutablePoint(int x, int y) { X = x; Y = y; }
    
    public ImmutablePoint MoveRight() => new ImmutablePoint(X + 1, Y);
}

Key Takeaways 🎯

  1. Stack = Fast & Automatic: Stack allocation is a simple pointer adjustment. Memory is automatically reclaimed when methods return. Perfect for local variables and short-lived data.

  2. Heap = Flexible but Managed: Heap allocation provides flexibility for objects with unpredictable lifetimes, but requires garbage collection. Minimize allocations in performance-critical code.

  3. Value Types Live Where Declared: Value types (struct, int, etc.) have no fixed locationβ€”they live on the stack when declared as local variables, but on the heap when inside a class.

  4. Reference Types Split Storage: Reference type variables store addresses (stack) pointing to objects (heap). Multiple variables can reference the same object.

  5. Allocation Has Real Costs: Every heap allocation creates work for the garbage collector. Profile your application and optimize hot paths by:

    • Using structs for small, immutable data
    • Reusing objects with pooling
    • Avoiding boxing in collections
    • Minimizing string allocations
  6. Understanding Enables Optimization: You can't optimize what you don't understand. Knowing stack vs heap allocation patterns allows you to make informed decisions about type choices (struct vs class), parameter passing (by value vs ref), and memory-sensitive algorithms.

πŸ”§ Try This: Memory Detective Exercise

Analyze this code and identify all allocations:

public class GameManager
{
    private List<int> scores = new List<int>();
    
    public void ProcessFrame()
    {
        int frameCount = 0;
        Player player = new Player();
        Vector2 position = new Vector2(10, 20);
        
        for (int i = 0; i < 100; i++)
        {
            scores.Add(i);
        }
    }
}

public struct Vector2
{
    public float X, Y;
    public Vector2(float x, float y) { X = x; Y = y; }
}

Stack allocations:

  • frameCount (int)
  • player (reference to heap object)
  • position (Vector2 struct, 8 bytes)
  • i (loop counter)

Heap allocations:

  • new Player() object
  • Potential internal array resizing in List<int> as it grows

No allocations:

  • The Vector2 struct stays on the stack
  • Adding ints to List<int> doesn't box (generic type)

πŸ“‹ Quick Reference Card

Concept Stack Heap
Speed ⚑ Very fast (pointer adjust) 🐒 Slower (search + allocate)
Cleanup βœ… Automatic (frame pop) πŸ”„ GC-managed
Size ~1 MB limit Gigabytes available
Thread Safety βœ… Per-thread ⚠️ Shared (needs sync)
Stores Local value types, references All reference type objects
Organization LIFO, contiguous Fragmented, compacted by GC

Memory Decision Tree:

Is it a class? ───→ YES ───→ Heap
      β”‚
      NO
      ↓
Is it inside a class? ───→ YES ───→ Heap (with object)
      β”‚
      NO
      ↓
   Stack (local value type)

Performance Rules:

  • Use struct for <16 bytes, immutable data
  • Use class for >16 bytes, mutable data
  • Pass large structs with ref or in
  • Avoid boxing (use generics)
  • Minimize allocations in hot paths
  • Profile before optimizing!

πŸ“š Further Study

  1. Microsoft Docs - Memory Management: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals - Official documentation on CLR memory management and garbage collection

  2. Performance Best Practices: https://learn.microsoft.com/en-us/dotnet/framework/performance/performance-tips - Microsoft's guide to writing high-performance C# code

  3. Pro .NET Memory Management by Konrad Kokosa: https://prodotnetmemory.com/ - Deep dive into CLR internals, GC algorithms, and memory optimization techniques

Mastering stack and heap allocation transforms how you write C#. You'll make better type choices, write more efficient code, and debug memory issues with confidence. Keep practicing with real code, use memory profilers, and always measure before optimizing! πŸš€