Course contentsShow
← C#
Lesson 314 of 1,9083. Generics and Collections ArchitectureFree lesson

Custom Allocation Strategies

Implement custom allocators for collections to use off-heap or pooled memory regions.

In this lesson, you will learn how to bypass the standard .NET Garbage Collector by implementing custom allocation strategies for your collections using NativeMemory.

Moving Beyond the Heap

Normally, when you add an element to a collection like a List<T>, the CLR allocates memory on the managed heap. If your collection grows frequently, this triggers GC cycles and potential fragmentation. A Custom Allocation Strategy allows you to take control of where and how that memory is obtained, typically by requesting raw blocks from the operating system.

Implementing an Off-Heap Buffer

To implement this, we use System.Runtime.InteropServices.NativeMemory. Instead of relying on new T[], we manually allocate a block of unmanaged memory, store the pointer, and manage the collection’s capacity manually.

Consider a simple custom buffer structure:

public unsafe struct UnmanagedBuffer<T> where T : unmanaged
{
    private T* _data;
    private int _capacity;

    public UnmanagedBuffer(int capacity)
    {
        _capacity = capacity;
        // Allocate raw memory outside the GC's view
        _data = (T*)NativeMemory.Alloc((nuint)capacity, (nuint)sizeof(T));
    }

    public void Dispose()
    {
        // Must manually free memory to avoid leaks
        NativeMemory.Free(_data);
    }
    
    public ref T this[int index] => ref _data[index];
}

Why Do This?

  1. Zero GC Pressure: Because the memory lives off-heap, the GC does not track these objects. This is crucial for high-frequency collections that would otherwise cause "Large Object Heap" (LOH) fragmentation.
  2. Deterministic Cleanup: You decide exactly when the memory is released via Dispose(), rather than waiting for a GC sweep.
  3. Cache Locality: By managing the layout yourself, you can ensure your data is packed tightly, which improves CPU cache hit rates during iteration.

The Trade-off

Manual allocation is a "power-user" feature. You lose the safety net of the GC. If you fail to call Free(), you create a memory leak that will persist for the lifetime of your application process. Always pair custom allocators with IDisposable and ensure your Dispose pattern is robust.

Key Takeaway: Custom allocators bypass the managed heap using unmanaged memory, providing deterministic memory control at the cost of manual lifecycle management.