Introduction
When I started diving into performance in C#, I realised there’s a hidden layer behind every line of code, particularly in terms of memory usage. The way we store, access, and pass data around can mean the difference between a smooth app and a laggy mess.
So, let’s go on a recap starting from the basics and building up to some of the coolest features in modern C#, like Span<T>.
The Foundation — Stack and Heap
Before we can talk about Span<T> or even “contiguous memory,” we need to know where our data lives.
Stack
- Fast, short-lived storage
- Holds local variables, method parameters, and most value types
- Works like a stack of plates (Last In, First Out)
- Gets cleared automatically when a method ends
Heap
- Slower but flexible storage
- Holds reference types (objects, arrays, strings)
- Managed by the garbage collector (GC)
- Data here can live far longer than the method that created it
Think of the stack as a desk in front of you — quick access but limited space. The heap is a big warehouse, but you need a forklift (GC) to manage it.
struct — The Lightweight Data Container
Now that we know about the stack, let’s meet its close friend: struct.
A struct is,
- A value type
- Usually stored on the stack
- Passed by copy, not by reference
- Great for small, lightweight data
public struct Point(int x, int y)
{
public int X = x, Y = y;
}
Point p1 = new(3, 4);
Point p2 = p1; // Copy — p2 is independent
p2.X = 10;
Console.WriteLine($"p1: ({p1.X}, {p1.Y})");
Console.WriteLine($"p2: ({p2.X}, {p2.Y})");
///////////////////////////////////////////////////////
Output:
p1: (3, 4)
p2: (10, 4)
This clearly shows that p1 stays unchanged after modifying p2, proving that structs are copied by value.
Contiguous Memory — The Secret to Speed
Some data structures store items in contiguous memory, meaning all elements sit right next to each other in one block.
[ 10 ][ 20 ][ 30 ][ 40 ]
Why is this good?
- The CPU can prefetch data efficiently
- Slicing (taking part of the data) is faster
- Fewer memory lookups
Why List<T> Is Contiguous… But Not Quite?
At first glance, List<T> seems contiguous; it wraps an internal array.
But there’s a catch.
- The List object itself lives on the heap
- It holds a reference to an internal array (which is contiguous)
- When the List grows, it allocates a new array and copies elements over
So, List<T> can’t directly be treated as a block of memory the way arrays or Span<T> can.
Span<T> — Memory Manipulation Without the Cost
Span<T> lets you work with slices of contiguous memory without creating new arrays or copying data.
Traditionally, in C#,
- If you wanted part of an array, you’d copy it to a new array.
- That meant extra allocations → more work for the GC.
Span<T> changes this.
- It represents a contiguous region of memory (stack, heap, or unmanaged memory).
- You can create a slice without copying.
int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> part = numbers.AsSpan(1, 3); // points to 20, 30, 40
- Changing part[0] also changes numbers[1] — they’re literally pointing to the same memory.
- No new array, no GC hit, no wasted CPU cycles.
Why Span<T> Is a ref struct?
Unlike normal structs, Span<T> is a ref struct, which means.
- It must live on the stack
- Can’t be stored in the heap
- Can’t be used in async methods or lambdas
Why?
Because Span<T> points directly into memory, letting it move to the heap would risk dangling pointers and unsafe memory access.
Final Takeaways
By understanding,
- Stack vs Heap: where data lives
- Structs: lightweight value containers
- Contiguous Memory: Why Sequential Storage Is Fast
- List<T> limitations: when “contiguous” isn’t quite
- Span<T>: how to slice memory without allocations
Thank You, and Stay Tuned for More!
More Articles from my Account.