Files
Misaki.HighPerformance/docs/documents/articles/Misaki.HighPerformance.LowLevel/allocators.md

12 KiB

Allocators

Every allocation in this library goes through an AllocationHandle. The handle determines where memory comes from, how it's organized, and when it's reclaimed. There is no default allocator — you always choose one explicitly.

AllocationHandle

AllocationHandle is a struct with a state pointer and three function pointers:

AllocationHandle
  _state   : void*    — allocator-specific context
  _alloc   : delegate — allocate
  _realloc : delegate — reallocate
  _free    : delegate — free

The function pointers let any allocator implementation be wrapped without boxing, virtual dispatch, or GC pressure. To create a custom allocator, you only need to populate this struct.

Every collection stores the AllocationHandle it was created with and uses it for all internal memory operations.

Built-in allocators

The library ships with allocators managed by AllocationManager:

Allocator Handle access Backing Lifetime Reuse Thread safety
Heap AllocationHandle.Persistent Heap (replaced by mimalloc if MHP_ENABLE_MIMALLOC is defined) Until freed Controlled by malloc or mimalloc Yes
FreeList AllocationHandle.FreeList Heap Until freed Yes — reuses freed blocks Yes (remote-free queue)
TLSF AllocationHandle.TLSF Virtual memory chunks Until freed Yes — low fragmentation Yes (lock-protected)
VirtualArena AllocationHandle.Temp Virtual memory (reserve on init, commit on demand) Until ResetTempAllocator() No — bulk reset only Yes
VirtualStack AllocationManager.CreateStackScope().AllocationHandle Virtual memory (reserve on init, commit on demand) Via VirtualStack.Scope Yes when scope disposed No — thread-local

The library also provides heap-based variants of Arena and Stack for direct use via MemoryPool:

Allocator Backing Lifetime Reuse
Arena Heap (NativeMemory.Alloc) Until Reset() or dispose No — bulk reset only
Stack Heap (NativeMemory.Alloc) Via Stack.Scope Yes when scope disposed

VirtualArena (Temp)

VirtualArena reserves a large virtual address range on initialization and commits physical memory on demand as allocations are made. Allocation bumps an offset pointer — there is no free-list walk, no block splitting, and no metadata search. This makes it the fastest allocator in the library.

Free is a no-op. Individual frees do nothing. The entire arena is reset at once by calling AllocationManager.ResetTempAllocator(), which rewinds the offset back to zero. This makes the arena ideal for frame-scoped or phase-scoped work where you want to allocate freely and discard everything at once.

// Temp allocations are freed collectively.
var a = new UnsafeArray<int>(10, AllocationHandle.Temp);
var b = new UnsafeList<int>(AllocationHandle.Temp);

// Reset everything.
AllocationManager.ResetTempAllocator();

// Both a and b are now invalid.

Under the hood, Temp uses a MemoryPool<VirtualArena>. The arena uses 64 KB pages for OS-level commit granularity and is thread-safe with a lock-free bump-allocate path.

FreeList (FreeList)

The free-list allocator reclaims and reuses individual blocks. When you free a block, it is returned to a size-bucketed free list and can satisfy a future allocation of the same bucket. This avoids the overhead of re-committing virtual memory while keeping fragmentation low.

FreeList uses per-thread caches for the hot path and a remote-free queue for cross-thread deallocation. This means threads can free each other's allocations without a global lock on the common path.

// FreeList allocations can be freed and reused independently.
var a = new UnsafeArray<int>(10, AllocationHandle.FreeList);
a.Dispose(); // Memory goes back to the free list.

var b = new UnsafeArray<int>(10, AllocationHandle.FreeList);
// Likely backed by the same memory as a.

TLSF (Persistent)

The Two-Level Segregated Fit allocator guarantees O(1) allocation and deallocation with very low external fragmentation. It organizes free blocks by size class in a two-level bitmap index, which lets it find a best-fit block in constant time. TLSF backs its memory pool with virtual memory chunks allocated via Mmap.

AllocationHandle.Persistent maps to the manager's internal TLSF allocator. Use it for long-lived allocations where fragmentation matters and where you need consistent O(1) performance.

The TLSF implementation is single-threaded internally and wrapped in a lock by the manager. For concurrent use from multiple threads, access is serialized through that lock.

// Persistent (TLSF) for long-lived allocations.
var cache = new UnsafeHashMap<int, EntityData>(
    AllocationHandle.Persistent
);

// ... use for the lifetime of the application ...

cache.Dispose();

VirtualStack (Stack)

VirtualStack is a LIFO allocator backed by a reserved virtual address range that commits physical memory on demand. It allocates by bumping an offset (like VirtualArena) but adds a scope mechanism that rewinds to a saved position on dispose.

The stack is not thread-safe and is designed for single-threaded or thread-local contexts. Each thread gets its own stack through AllocationManager.CreateStackScope():

// Creates a thread-local stack scope.
// The scope's AllocationHandle can be used for allocations
// that are automatically reclaimed when the scope ends.
using var scope = AllocationManager.CreateStackScope();

// Allocate from the stack.
var temp = new UnsafeArray<int>(10, scope.AllocationHandle);

// When scope is disposed, all allocations are rewound.

The stack uses 64 KB commit granularity and supports scope nesting. The scope records the offset at creation and rewinds to it on dispose — allocations from an inner scope are always reclaimed before the outer scope.

Arena (heap)

Arena is the heap-based counterpart of VirtualArena. It uses NativeMemory.Alloc to allocate a fixed-size buffer on the heap and bumps an offset pointer for allocations. It supports the same bulk-reset pattern but without virtual memory reservation.

using var arena = new MemoryPool<Arena, Arena.CreationOptions>(
    new Arena.CreationOptions { size = 1024 * 1024 });

var arr = new UnsafeArray<int>(10, arena.AllocationHandle);

// Reset rewinds the offset. Memory stays allocated.
arena.Allocator.Reset();

Arena is thread-safe with a lock-free bump-allocate path.

Stack (heap)

Stack is the heap-based counterpart of VirtualStack. It uses NativeMemory.Alloc to allocate a fixed-size buffer on the heap and uses the same scope mechanism to rewind allocations on scope dispose.

using var stack = new MemoryPool<Stack, Stack.CreationOptions>(
    new Stack.CreationOptions { size = 1024 * 1024 });

using (var scope = stack.Allocator.CreateScope(stack.AllocationHandle))
{
    var arr = new UnsafeArray<int>(10, scope.AllocationHandle);
} // Scope dispose rewinds all allocations.

Stack is not thread-safe and is designed for single-threaded contexts.

AllocationManager configuration

AllocationManager can be configured with an AllocationManagerDesc to control the capacity and alignment of each built-in allocator:

var desc = new AllocationManagerDesc
{
    ArenaCapacity = 1024 * 1024 * 1024,     // 1 GB virtual reservation
    StackCapacity = 32 * 1024 * 1024,       // 32 MB per thread
    FreeListChunkSize = 64 * 1024,          // 64 KB chunks
    FreeListDefaultAlignment = 16,          // 16-byte alignment
    TLSFAlignment = 16,                     // 16-byte alignment
    TLSFInitialChunkSize = 64 * 1024 * 1024 // 64 MB initial chunk
};

AllocationManager.Initialize(desc);

Calling Initialize() with no arguments uses these same defaults.

MemoryPool for scoped allocators

MemoryPool<TAllocator, TOpts> creates a standalone allocator outside of AllocationManager. This is useful when you want:

  • An allocator type not available in the built-in set
  • An allocator scoped to a single method or algorithm
  • Isolation from the global allocation state
using var pool = new MemoryPool<TLSF, TLSF.CreationOptions>(
    new TLSF.CreationOptions
    {
        alignment = 16,
        initialChunkSize = 1024 * 1024
    });

using var array = new UnsafeArray<int>(10, pool.AllocationHandle);

// When pool is disposed, all TLSF memory is released.

The pool wraps any type implementing IMemoryAllocator<TSelf, TOpts>. This includes Arena, VirtualArena, Stack, VirtualStack, TLSF, and DynamicArena:

// Heap-based arena.
using var arenaPool = new MemoryPool<Arena, Arena.CreationOptions>(
    new Arena.CreationOptions { size = 1024 * 1024 });

// Virtual-memory-based stack.
using var stackPool = new MemoryPool<VirtualStack, VirtualStack.CreationOptions>(
    new VirtualStack.CreationOptions { reserveCapacity = 1024 * 1024 });

// Dynamically growing arena (heap).
using var dynamicPool = new MemoryPool<DynamicArena, DynamicArena.CreationOptions>(
    new DynamicArena.CreationOptions { initialSize = 4096 });

DynamicArena creates linked arenas that grow automatically when full, with no virtual address reservation upfront.

Custom allocators

Creating a custom allocator requires populating an AllocationHandle with your own allocate, reallocate, and free functions:

static void* MyAlloc(void* state, nuint size, nuint alignment, AllocationOption option)
{
    // Your allocation logic.
}

static void* MyRealloc(void* state, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption option)
{
    // Your reallocation logic.
}

static void MyFree(void* state, void* ptr)
{
    // Your deallocation logic.
}

var handle = new AllocationHandle(
    myAllocatorState,
    &MyAlloc,
    &MyRealloc,
    &MyFree
);

var array = new UnsafeArray<int>(10, handle);

For more structured custom allocators, implement IMemoryAllocator<TSelf, TOpts> and use MemoryPool<TAllocator, TOpts>:

public unsafe struct MyAllocator
    : IMemoryAllocator<MyAllocator, MyAllocator.CreationOptions>
{
    public struct CreationOptions { /* ... */ }

    public static MyAllocator Create(in CreationOptions opts) { /* ... */ }
    public void* Allocate(nuint size, nuint alignment, AllocationOption option) { /* ... */ }
    public void* Reallocate(void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption option) { /* ... */ }
    public void Free(void* ptr) { /* ... */ }
    public void Dispose() { /* ... */ }
}

using var pool = new MemoryPool<MyAllocator, MyAllocator.CreationOptions>(/* ... */);

AllocationOption

AllocationOption is a flags enum that controls per-allocation behavior:

Value Behavior
None Memory is returned as-is, contents are undefined
Clear All allocated bytes are zeroed before returning
// Request zeroed memory.
var ptr = handle.Alloc(1024, 16, AllocationOption.Clear);

Clear is useful for security-sensitive data or when you need deterministic initialization. Omitting it avoids the cost of touching every page.

Enable Mimalloc

You can define MHP_ENABLE_MIMALLOC to use mimalloc as the underlying allocator for AllocationHandle.Persistent and MemoryUtility.Malloc instead of the default C allocator.

Using mimalloc requires to install the TerraFX.Interop.Mimalloc package.

Additional resources