refactor project structure and add documents.

This commit is contained in:
2026-05-14 02:00:09 +09:00
parent a0c0231613
commit f4a73099a0
963 changed files with 957378 additions and 1366 deletions

View File

@@ -0,0 +1,278 @@
# 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.
```csharp
// 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.
```csharp
// 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.
```csharp
// 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()`:
```csharp
// 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.
```csharp
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.
```csharp
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:
```csharp
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
```csharp
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`:
```csharp
// 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:
```csharp
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>`:
```csharp
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 |
```csharp
// 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
- [Introduction](introduction.md) — install, first steps, and safety checks
- [Architecture overview](architecture-overview.md) — layering, MemoryHandle, and struct semantics
- [Collection types](collection-types.md) — all available data structures

View File

@@ -0,0 +1,123 @@
# Architecture overview
The library is structured as a stack of explicit layers. Each layer has a single responsibility, and you can work at any level depending on how much control you need.
```
User Code
|
Unsafe collections (UnsafeArray, UnsafeList, UnsafeHashMap, ...)
|
AllocationHandle (function-pointer-based "interface")
|
Allocators (Arena / FreeList / TLSF / Stack)
|
OS memory (VirtualAlloc / malloc / mimalloc)
```
## Design philosophy
The library is built around four principles:
**Explicit over implicit.** Every allocation requires an `AllocationHandle`. There is no default allocator, no hidden malloc, and no GC fallback. If memory is allocated, you chose exactly where it came from.
**Unsafe-first.** All collections work with raw pointers internally. Safety checks are optional and compiled away in release. The library trusts you — and lets you prove you can be trusted.
**Struct-only collections.** No managed objects, no handles to GC-tracked state, no hidden heap allocations. A collection is just a pointer + metadata. Copy it, inline it, store it in unmanaged memory.
**Zero overhead by default.** Safety checks, stack traces, tracking — all opt-in via compile-time constants. Release builds produce the same code as hand-written pointer manipulation.
## AllocationHandle pattern
`AllocationHandle` is the central abstraction. It is a struct containing a state pointer and three function pointers:
```
AllocationHandle
_state : void* — allocator-specific context
_alloc : delegate — allocate
_realloc : delegate — reallocate
_free : delegate — free
```
Because it uses function pointers instead of virtual interfaces, an `AllocationHandle` call is a direct indirect call — no boxing, no vtable lookup, no GC pressure. Any combination of alloc/free/realloc functions can be composed into a handle, which means custom allocators are just a matter of filling in the struct.
Every collection stores its `AllocationHandle` and uses it for all internal memory operations.
## MemoryHandle tracking
`MemoryHandle` is a safety-only struct that pairs an allocation ID with a generation counter:
```csharp
public readonly struct MemoryHandle
{
public readonly int ID;
public readonly int Generation;
}
```
When `MHP_ENABLE_SAFETY_CHECKS` is defined, every allocation is registered in `AllocationManager`'s tracking database with its address and size. Operations like dispose verify the handle is still valid, catching double-free and use-after-free errors. The generation counter prevents handle reuse after an allocation is freed.
In release builds, `MemoryHandle` fields compile away to nothing.
## AllocationManager
`AllocationManager` serves two roles:
- **Registry.** It owns the global instances of the built-in allocators (`Temp`, `FreeList`, `Persistent`, `TLSF`). Calling `AllocationHandle.Persistent` returns a handle to the manager's internal TLSF allocator instance.
- **Safety database.** When safety checks are enabled, the manager tracks every live allocation. Diagnostics, snapshot inspection, and leak detection all go through this system.
The manager must be initialized before any allocation and disposed at shutdown:
```csharp
AllocationManager.Initialize();
// ... use collections ...
AllocationManager.Dispose();
```
## MemoryPool for scoped allocators
`MemoryPool<TAllocator, TOpts>` creates a standalone allocator scoped to a method or algorithm, independent of `AllocationManager`. This is useful when you want an allocator that doesn't exist in the built-in set, or you want to isolate allocations from the global state:
```csharp
using var pool = new MemoryPool<TLSF, TLSF.CreationOptions>(
new TLSF.CreationOptions { alignment = 16 });
using var array = new UnsafeArray<int>(10, pool.AllocationHandle);
```
Collections work with any `AllocationHandle`, regardless of whether it came from `AllocationManager` or a `MemoryPool`. You do not need to initialize `AllocationManager` when using only standalone pools. The allocator lives as long as the pool, and all its memory is released when the pool is disposed.
## Struct semantics
All collections are structs with no managed references. This has two important consequences:
- **No GC overhead.** The struct itself is stack-allocated or inlineable. The memory it points to is unmanaged and outside the GC's view.
- **Pass by value copies the struct.** If you pass an `UnsafeList<T>` to a method without `ref`, the method operates on a copy. Additions, removals, and resizes on that copy are invisible to the caller. Always use `ref` for mutation:
```csharp
public void Process(ref UnsafeList<int> list) { ... }
```
## Safety checks system
The library supports two compile-time safety levels:
- `MHP_ENABLE_SAFETY_CHECKS` — enables bounds checking, use-after-free detection, double-free detection, and `IsCreated` validity verification (checks that the internal memory handle is still registered in the tracking database).
- `MHP_ENABLE_STACKTRACE` — adds stack trace capture on every allocation, enabling precise leak investigation. Requires `MHP_ENABLE_SAFETY_CHECKS`.
When `MHP_ENABLE_SAFETY_CHECKS` is not defined, the safety fields compile away and `IsCreated` only checks whether the internal pointer is non-null without verifying the actual validity of the memory. This matches the performance of raw pointer code.
## AllocationOption
Allocation operation can take an optional `AllocationOption`:
| Value | Behavior |
|---|---|
| `None` | Default — memory is returned as-is |
| `Clear` | Zero the allocated memory before returning |
## Content files packaging
The library is packaged as content files rather than a traditional assembly. Source files are embedded directly into the consuming project at build time. This enables the AOT compiler to see through every call site, inline aggressively, and strip unused code paths — including the entire safety check infrastructure when the relevant constants are undefined.

View File

@@ -0,0 +1,77 @@
# Collection types
All collection types in this library are structs that wrap unmanaged memory allocated through an `AllocationHandle`. They follow the same general API patterns as the BCL collections but operate entirely outside the GC heap.
## Array-like types
| Data structure | Description |
|---|---|
| `UnsafeArray<T>` | A fixed-size array. Supports resize via `Resize()`. |
| `UnsafeList<T>` | A dynamically resizing list. |
| `UnsafeQueue<T>` | A FIFO queue. |
| `UnsafeStack<T>` | A LIFO stack. |
| `UnsafeChunkedList<T>` | A list that stores elements in fixed-size chunks. Adding elements never moves existing ones, providing stable element addresses. |
## Map and set types
| Data structure | Description |
|---|---|
| `UnsafeHashMap<TKey, TValue>` | An unordered associative array of key-value pairs. |
| `UnsafeHashSet<T>` | A set of unique values. |
| `UnsafeMultiHashMap<TKey, TValue>` | An unordered associative array where keys don't have to be unique. Multiple values can share the same key. |
## Sparse types
| Data structure | Description |
|---|---|
| `UnsafeSparseSet<T>` | A sparse set that provides O(1) insertion, deletion, and lookup. Uses the dense/sparse array pattern. Sparse indices work like entity IDs and are automatically generated. |
| `UnsafeSlotMap<T>` | A slot map with generation counters. Fast insertion, removal, and lookup by slot index. The generation counter prevents stale index access to data that has been replaced. |
## String and text types
| Data structure | Description |
|---|---|
| `FixedString32` | A 32-byte UTF-16 string (16 characters max). |
| `FixedString64` | A 64-byte UTF-16 string (32 characters max). |
| `FixedString128` | A 128-byte UTF-16 string (64 characters max). |
| `FixedString256` | A 256-byte UTF-16 string (128 characters max). |
| `FixedString512` | A 512-byte UTF-16 string (256 characters max). |
| `FixedString1024` | A 1024-byte UTF-16 string (512 characters max). |
| `FixedString2048` | A 2048-byte UTF-16 string (1024 characters max). |
| `FixedString4096` | A 4096-byte UTF-16 string (2048 characters max). |
| `FixedText32` | A 32-byte UTF-8 encoded string (30 bytes max). |
| `FixedText64` | A 64-byte UTF-8 encoded string (62 bytes max). |
| `FixedText128` | A 128-byte UTF-8 encoded string (126 bytes max). |
| `FixedText256` | A 256-byte UTF-8 encoded string (254 bytes max). |
| `FixedText512` | A 512-byte UTF-8 encoded string (510 bytes max). |
| `FixedText1024` | A 1024-byte UTF-8 encoded string (1022 bytes max). |
| `FixedText2048` | A 2048-byte UTF-8 encoded string (2046 bytes max). |
| `FixedText4096` | A 4096-byte UTF-8 encoded string (4094 bytes max). |
All fixed string and text types are stack-only. Every copy duplicates the underlying data.
## Parallel types
| Data structure | Description |
|---|---|
| `UnsafeParallelQueue<T>` | A dynamically resizing, lock-free queue. Provides `ParallelProducer` and `ParallelConsumer` views for safe concurrent access. Uses a spin lock only during chunk allocation. |
| `UnsafeParallelHashMap<TKey, TValue>` | A parallel hash map. Provides a `ParallelWriter` for concurrent insertions from multiple threads. Does not resize concurrently — pre-allocate enough capacity. |
## Bit structures
| Data structure | Description |
|---|---|
| `UnsafeBitSet` | An arbitrary-sized array of bits with set, test, clear, and search operations. |
## Utility types
| Type | Description |
|---|---|
| `ReadOnlyUnsafeCollection<T>` | A read-only view over a pointer and count. Implicitly converts to `ReadOnlySpan<T>`. Useful for passing collection data to APIs that expect spans. |
| `DisposablePtr<T>` | A pointer wrapper that calls `Dispose` on the pointed-to value when disposed. Used by allocate-on-heap factory methods like `UnsafeParallelQueue<T>.Allocate()`. |
## Additional resources
- [Introduction](introduction.md) — install, first steps, and safety checks
- [Architecture overview](architecture-overview.md) — layering, AllocationHandle, and struct semantics
- [Allocators](allocators.md) — built-in allocators, MemoryPool, and custom allocators

View File

@@ -0,0 +1,66 @@
# Introduction
The low-level library provides unsafe collections, allocators, and memory-management primitives for high-performance C#. It gives you explicit control over allocation, layout, and ownership so you can build systems that run without GC interference.
## Why a dedicated low-level library?
Standard .NET memory management wasn't designed for allocation-heavy game and simulation workloads:
- `NativeMemory.Alloc` and `Marshal.AllocHGlobal` provide raw allocation but no collection types, no lifetime tracking, and no safety checks.
- The BCL collections (`List<T>`, `Dictionary<K,V>`) allocate on the managed heap, producing GC pressure in tight loops.
- `Span<T>` and `Memory<T>` avoid allocations but don't own their memory and can't manage lifetimes across asynchronous boundaries.
This library solves these problems with pluggable allocators, unsafe collections that wrap raw pointers, and a safety check system that can be compiled away in release builds.
## Feature highlights
| Feature | Description |
|---|---|
| Pluggable allocators | Every allocation passes through an `AllocationHandle` — choose the right allocator per use case |
| Built-in allocators | `Temp`, `FreeList`, `Persistent`, `TLSF` — or use `MemoryPool` with heap-based `Arena` and `Stack` |
| Unsafe collections | Arrays, lists, queues, stacks, hash maps, hash sets, sparse sets, slot maps, chunked lists |
| Parallel-aware types | Lock-free queue and concurrent hash map with parallel reader/writer views |
| Fixed-size text | Stack-only `FixedString` (UTF-16) and `FixedText` (UTF-8) for zero-allocation string operations |
| Compile-time safety | `MHP_ENABLE_SAFETY_CHECKS` enables bounds checking, use-after-free detection, and leak tracking — compiled away in release |
| Custom allocators | Implement your own allocator by populating an `AllocationHandle` with function pointers |
| `MemoryPool<TAllocator>` | Scope allocators to a method or algorithm without touching the global state |
| Struct semantics | All collections are structs with no managed handles — no GC overhead, pass by `ref` for mutation |
## Basic usage
```csharp
using Misaki.HighPerformance.LowLevel.Buffer;
AllocationManager.Initialize();
var array = new UnsafeArray<int>(10, AllocationHandle.Persistent);
array[0] = 42;
Console.WriteLine(array[0]); // Output: 42
array.Dispose();
AllocationManager.Dispose();
```
## Who this is for
- Custom game engine developers who need allocation control without GC pauses
- Systems programmers building runtime components, job schedulers, or custom allocators
- .NET developers who have hit performance limits with managed collections in hot paths
## Requirements
- .NET 10.0 or later
- `unsafe` code enabled (`<AllowUnsafeBlocks>true</AllowUnsafeBlocks>`)
## Install
```bash
dotnet add package Misaki.HighPerformance.LowLevel
```
## Additional resources
- [Architecture overview](architecture-overview.md) — understand the allocation model and design philosophy
- [Allocators](allocators.md) — learn about each built-in allocator and how to create custom ones
- [Collection types](collection-types.md) — explore all available data structures

View File

@@ -0,0 +1,8 @@
- name: Introduction
href: introduction.md
- name: Architecture overview
href: architecture-overview.md
- name: Allocators
href: allocators.md
- name: Collection types
href: collection-types.md