|
1 | 1 | # MemoryPrototype |
2 | 2 |
|
3 | | -**MemoryPrototype** is a prototype memory allocation and handle system built to experiment with custom memory management in C#. Inspired by techniques found in game engines and real-time systems, it explores handle indirection, dual-tier memory lanes, and optional compaction. |
| 3 | +**MemoryPrototype** is a high-performance, dual-tier unmanaged memory arena and handle system built to bypass the .NET Garbage Collector for performance-critical subsystems. Inspired by data-oriented memory management architecture in low-latency runtime engines, it implements stable handle indirection, generational version validation, automated lifecycle eviction policies, and low-level compacting memory lanes. |
4 | 4 |
|
5 | | -> ⚠️ **Note:** This is a **prototype for learning and experimentation**. Use at your own risk. |
6 | | -> Validated via high-stress memory pressure tests and performance benchmarks against the .NET Garbage Collector. |
| 5 | +> ⚠️ **Engineering Status:** This is an **experimental sandbox prototype for learning and systems engineering**. While optimized to achieve exceptional throughput and zero GC allocation on hot paths, it is currently undergoing architectural hardening. See the [Production Finish-Line Roadmap](#-the-production-finish-line-roadmap) below for critical hurdles to address before running this engine in production environments. |
7 | 6 |
|
8 | 7 | --- |
9 | 8 |
|
10 | | -### 🚀 The Killer Features |
| 9 | +### 🚀 Key Features |
11 | 10 |
|
12 | 11 | 1. **Stable Handle Indirection (Relocatable Memory)** |
13 | | - Instead of handing you a raw pointer that permanently pins memory, MemoryLane gives you an O(1) `MemoryHandle`. This means the engine can physically move your data in the background to defragment the heap, and your references will never break. |
14 | | -2. **The "Janitor" (Automated Lifecycle Management)** |
15 | | - You don't have to decide where data lives. Hot, short-lived data stays in the zero-allocation `FastLane`. If data survives too long or goes "Cold," the automated Janitor seamlessly migrates it to the persistent `SlowLane`, leaving a lightweight redirection stub behind. |
16 | | -3. **Live Compaction (Zero External Fragmentation)** |
17 | | - Standard freelist allocators suffer from "Swiss Cheese" memory over time, leaving gaps you can't use for large objects. MemoryLane features Live Compaction, physically sliding memory blocks together to eliminate gaps and reclaim 100% of your wasted space without pausing the application. |
18 | | -4. **Pluggable Allocation Strategies** |
19 | | - Not all workloads are the same. MemoryLane allows you to hot-swap the internal allocation engine of the `FastLane` via a simple config toggle. Choose between a safe, hole-reusing **Free-List Allocator** or a lightning-fast, O(1) **Linear Bump Allocator**. |
| 12 | + Instead of handing out raw pointers that permanently pin your layout blocks in memory, `MemoryArena` yields opaque, lightweight $O(1)$ `MemoryHandle` tokens. This layer of indirection permits the background engine to safely move your data to defragment the unmanaged heap without ever breaking user references. |
20 | 13 |
|
21 | | ---- |
22 | | - |
23 | | -## ⚠️ Known Limitations |
24 | | - |
25 | | -- Does not manage C# objects directly — only unmanaged memory blocks. |
26 | | -- No garbage collection integration. |
27 | | -- No automatic bounds checking — `MemoryHandle` is mostly safe *if* used correctly via the API. |
28 | | -- Designed for low-level experimentation, not high-level safety. |
29 | | - |
30 | | ---- |
31 | | - |
32 | | -## ✨ Features |
33 | | - |
34 | | -- 🧠 **Dual Memory Lanes** - `FastLane`: low-latency, short-lived allocations (e.g., frame-local). |
35 | | - - `SlowLane`: large, persistent allocations (e.g., background assets). |
36 | | - |
37 | | -- 🔁 **Stable Handle System** - `MemoryHandle` provides opaque, safe references to internal memory entries. |
38 | | - - Redirection via lightweight stubs for safe relocation. |
39 | | - |
40 | | -- 🔌 **Modular Strategy Pattern** - Toggle the core allocation logic (`AllocatorStrategy.FreeList` vs `AllocatorStrategy.LinearBump`) to perfectly match your application's allocation tempo. |
41 | | - |
42 | | -- 🧹 **Optional Live Compaction** - Reduces fragmentation and reclaims space dynamically. |
| 14 | +2. **Generational Version Validation (Zombie Protection)** |
| 15 | + To defeat the dangling pointer problem common in custom allocators, each `MemoryHandle` carries a tracking `Id` and a generation `Version`. When handles are recycled, their index version increments. Any legacy handle trying to access an old address will instantly trigger a safe access violation instead of silently corrupting recycled memory. |
43 | 16 |
|
44 | | -- 🔄 **Stub-Based Indirection** - Moves memory without invalidating existing handles. |
| 17 | +3. **The "Janitor" (Automated Tiered Eviction)** |
| 18 | + You do not need to micro-manage object lifespans. Hot, short-lived data is allocated inside a lean, high-speed `FastLane`. If an allocation outstays its welcome (based on frame age), grows too large, or is explicitly tagged with `AllocationHints.Cold`, the automated Janitor moves it to a persistent `SlowLane` during quiet frames, replacing the original site with a seamless redirection stub. |
45 | 19 |
|
46 | | -- 🧪 **Safety Checks** - Includes `TryGet<T>`, `IsValid`, and `GetHandleState`. |
| 20 | +4. **Pluggable Fast-Lane Strategies** |
| 21 | + Not all workloads share the same structural tempo. The arena config file lets you hot-swap the allocation engine backing the `FastLane`: |
| 22 | + * **`AllocatorStrategy.FreeList`**: Tracks fragmented allocations with a block recycling manager; ideal for chaotic, highly variable lifespans. |
| 23 | + * **`AllocatorStrategy.Linear`**: A blazing-fast, true $O(1)$ sequential bump allocator; ideal for transient per-frame scratch pads. |
47 | 24 |
|
48 | | -- 🛤️ **One-Way Lane Transfer** - `OneWayLane` shifts data from `FastLane` to `SlowLane`. |
49 | | - - Uses `Span<T>` and `Buffer.MemoryCopy` to avoid breaking references. |
50 | | - - Can be plugged into compaction or run manually. |
51 | | - |
52 | | -- 📦 **High-Level Managed Types** - Includes ArenaList<T>, a resizable collection that lives entirely in unmanaged memory. |
53 | | - - Automatically handles Growth and Migration through the handle system. |
54 | | - - Integrates with the Janitor for automatic defragmentation when lists are resized or freed. |
55 | | - |
56 | | -- 🔤 **Unmanaged String Support** - Store strings as UTF-8 byte arrays to save 50% memory over C# UTF-16 strings and bypass the GC. |
57 | | - |
58 | | -- ✅ **Robust Unit Tests** - Includes structural validation, performance benchmarks, and multi-lane stress testing. |
| 25 | +5. **Atomic Safe Copying** |
| 26 | + Features a native, thread-safe `BulkSet<T>` utility that safely accepts `ReadOnlySpan<T>` payloads, guaranteeing that block data transfers, size validation, and pointer resolution occur in a single atomic phase safely isolated from compaction interrupts. |
59 | 27 |
|
60 | 28 | --- |
61 | 29 |
|
62 | | -## 🎯 Learning Objectives |
| 30 | +## 📐 Architecture Overview |
63 | 31 |
|
64 | | -This project was created to: |
| 32 | +MemoryPrototype establishes a strict separation of concerns across multiple specialized unmanaged memory tiers. |
65 | 33 |
|
66 | | -- Understand manual memory management strategies. |
67 | | -- Explore handle indirection and pointer safety. |
68 | | -- Learn arena-style memory management, paging, and pooling. |
69 | | -- Gain insights into low-level system design using C#. |
| 34 | +* **⚡ FastLane**: Optimized for high-frequency, short-lived "hot" transient structures. Uses sequential **positive integers** (`1, 2, 3...`) for allocation IDs. Version tracking is backed by an ultra-fast, growable native pointer array (`uint*`) that maps handle IDs directly to generation counts via an $O(1)$ array offset calculation. |
| 35 | +* **🐌 SlowLane**: Optimized for large, persistent "cold" allocations or long-lived structural blocks. Uses sequential **negative integers** (`-1, -2, -3...`) for allocation IDs. Maps negative handles cleanly to positive array coordinates using an absolute value index converter (`-id`), retaining raw pointer lookup performance. |
| 36 | +* **🪐 BlobManager (Slow-Lane Sub-Allocator)**: Optimized for isolating tiny, chaotic allocations (e.g., $\le$ 256 bytes) away from the primary free block table to prevent memory fragmentation. |
| 37 | +* **走 OneWayLane (The Bridge)**: Orchestrates seamless data migration. It safely reads the original entry metadata to pull telemetry (preserving original frame age, priorities, and usage hints) and executes a vector-speed `System.Buffer.MemoryCopy` across the lane boundaries before converting the old site into a routing redirection stub. |
70 | 38 |
|
71 | 39 | --- |
72 | 40 |
|
73 | | -## ⚙️ Requirements |
| 41 | +## 🛑 The Production Finish-Line Roadmap |
74 | 42 |
|
75 | | -- **.NET 9.0** (or compatible runtime) |
| 43 | +To move this system out of the prototyping phase and into a hardened, production-grade systems engine, the following structural limitations must be addressed: |
76 | 44 |
|
77 | | ---- |
| 45 | +### 1. Multi-Threaded Cache-Line Contention (The Global Lock Bottleneck) |
| 46 | +* **Current State:** The system achieves thread safety by acquiring a heavy global `lock (_lock)` on the wrapper for nearly every execution entry point (`Allocate`, `Resolve`, `Free`, `BulkSet`). Under high thread contention, parallel worker threads will stall waiting for this single lock, destroying core scaling. |
| 47 | +* **Production Fix:** Transition to a **Thread-Local Arena** architecture. Each thread should allocate out of its own dedicated lock-free local scratch ring, only accessing the synchronized global arena when local pages are completely exhausted. |
78 | 48 |
|
79 | | -## 🧭 Future Work / Ideas |
| 49 | +### 2. "Stop-the-World" Compaction Latency |
| 50 | +* **Current State:** When compaction triggers, the compactor engine allocates an entirely new unmanaged heap block via `Marshal.AllocHGlobal` and slides all survivors over via memory block copying. For massive heaps (e.g., 512MB+), this causes noticeable block stutters (latency spikes) and temporarily forces a **double memory footprint**. |
| 51 | +* **Production Fix:** Implement an **Incremental/Phased Compactor** that only relocates a small chunk of fragmented blocks per frame tick, or enforce strict zero-compaction rules where arenas are completely cleared and cycled out at deterministic boundaries (e.g., scene changes). |
80 | 52 |
|
81 | | -These are conceptual features or areas for future exploration: |
| 53 | +### 3. Hardware Memory Alignment Blindness |
| 54 | +* **Current State:** The bump allocators currently increment layouts directly by byte size (`_nextFreeOffset += size`). If a user allocates an odd structural layout (e.g., a 7-byte struct), subsequent items are placed on unaligned memory addresses, causing the CPU to fetch multiple cache-lines for single read instructions, tanking cache line efficiency. |
| 55 | +* **Production Fix:** Force all lane offsets to snap cleanly to hardware alignment boundaries (e.g., 16-byte, 32-byte, or 64-byte boundaries for SIMD alignment) using bitwise masking masks: |
| 56 | + $$\text{alignedOffset} = (\text{offset} + (\text{alignment} - 1)) \ \& \ \sim(\text{alignment} - 1)$$ |
82 | 57 |
|
83 | | -- [ ] **Thread Safety** — Replace global lock with `ReaderWriterLockSlim` for parallel Resolve operations. |
84 | | -- [ ] **Handle ID Pooling** — Implement a stack-based pool for `MemoryHandle` IDs to ensure O(1) allocation without metadata overhead. |
85 | | -- [ ] **SIMD Alignment** — Add `AlignTo(int boundary)` to the allocation logic for cache-line and SIMD-friendly memory offsets. |
86 | | -- [ ] **Visual Profiler** — Export internal memory maps to a heatmap (JSON/HTML) for real-time fragmentation monitoring. |
87 | | -- [ ] **Bidirectional Transfer** — (Advanced) Allow the Janitor to "pull" frequently accessed data back into the `FastLane`. |
88 | | -- [ ] **The "Lounge" (Pool Allocator)** — A dedicated memory tier for uniform, homogeneous data blocks (e.g., arrays of identical structs). Eliminates fragmentation entirely using an O(1) index stack. Includes a strict Janitor policy to evict "liars" to the `SlowLane` if their temporary frame data overstays its welcome. |
| 58 | +### 4. Zero Defensive Buffer-Overrun Safety Guardrails |
| 59 | +* **Current State:** Resolving a handle exposes a raw native `nint` address space. If code reads or writes outside the structural bounds of that specific entry allocation, it will silently overwrite adjacent allocations or metadata arrays without an error, resulting in impossible-to-trace heap corruption. |
| 60 | +* **Production Fix:** In `#if DEBUG` configurations, implement **Canary Guard Bands**. Inject a known sequence of invariant diagnostic bytes (e.g., `0xDEADBEEF`) directly before and after every physical allocation chunk. During `Free()` or compaction loops, validate these bands to instantly catch memory overruns. |
89 | 61 |
|
90 | 62 | --- |
91 | 63 |
|
92 | | -## 📐 Planned Architecture Overview |
93 | | - |
94 | | -MemoryLane utilizes a dual-tier strategy to bypass the .NET Garbage Collector for high-frequency or large-scale unmanaged data. By using Handle Indirection, the system can physically move memory (defragmentation) without breaking user-held references. |
95 | | - |
96 | | -### 🔧 Tier Responsibilities |
97 | | - |
98 | | -#### ✅ FastLane |
99 | | -**Optimized for:** High-frequency, short-lived "hot" data. |
100 | | -**Backend:** Pluggable Strategy (`LinearBump` for maximum O(1) speed, or `FreeList` for highly-variable, chaotic lifespans). |
101 | | -**Convention:** Uses Positive Allocation IDs. |
102 | | -**The Janitor:** Automatically promotes stale, oversized, or "Cold-tagged" entries to the SlowLane during maintenance cycles to keep the FastLane lean. |
103 | | - |
104 | | -#### ✅ SlowLane |
105 | | -**Optimized for:** Large, persistent "cold" data or background assets. |
106 | | -**Backend:** Hybrid Segregated Allocator (Free-Block Manager for large assets, Bump Allocator for tiny blobs). |
107 | | -**Convention:** Uses Negative Allocation IDs. |
108 | | -**Maintenance:** Performs deep compaction when fragmentation exceeds configured thresholds. |
109 | | - |
110 | | -#### ✅ OneWayLane (The Bridge) |
111 | | -**Responsibility:** Seamlessly migrates memory from FastLane to SlowLane. |
112 | | -**Mechanism:** Direct pointer-to-pointer `Buffer.MemoryCopy`. |
113 | | -**Stub System:** Replaces the FastLane entry with a Stub that redirects all future Resolve calls to the new SlowLane address. |
114 | | - |
115 | | ---- |
| 64 | +## 🧩 Config Presets & Advanced Usage |
116 | 65 |
|
117 | | -## 🧩 Example Usage |
| 66 | +MemoryPrototype includes optimized configuration presets out of the box to instantly tune execution properties for specific workloads: |
118 | 67 |
|
119 | 68 | ```csharp |
120 | 69 | using MemoryManager; |
121 | | -using MemoryManager.Types; |
122 | | - |
123 | | -// --- 1. Setup Configuration --- |
124 | | -// Define your sandbox boundaries. 10MB SlowLane, 1MB FastLane. |
125 | | -var config = new MemoryManagerConfig(slowLaneSize: 10 * 1024 * 1024) |
126 | | -{ |
127 | | - EnableAutoCompaction = true, |
128 | | - FastLaneUsageThreshold = 0.90, // Trigger maintenance at 90% full |
129 | | - MaxFastLaneAgeFrames = 600, // Janitor evicts "stale" data after 10 seconds (60fps) |
130 | | - FastLaneStrategy = AllocatorStrategy.LinearBump // O(1) lightning speed |
131 | | -}; |
132 | | - |
133 | | -var arena = new MemoryArena(config); |
134 | | - |
135 | | -// --- 2. High-Level Collections (The Lounge) --- |
136 | | -// Use ArenaList for resizable, unmanaged collections. |
137 | | -// It feels like a standard List<T>, but lives in your FastLane. |
138 | | -var list = new ArenaList<int>(arena, initialCapacity: 16); |
139 | | -for(int i = 0; i < 20; i++) list.Add(i); |
140 | | - |
141 | | -// --- 3. Stable Handles & Syntactic Sugar --- |
142 | | -// Store a single value. The handle remains valid even if the Janitor |
143 | | -// moves this integer to the SlowLane later! |
144 | | -var healthHandle = arena.AllocateAndStore(100); |
145 | | - |
146 | | -// --- 4. Bulk Data & Memory "Slamming" --- |
147 | | -int[] sourceData = { 10, 20, 30, 40, 50 }; |
148 | | -var arrayHandle = arena.AllocateArray<int>(sourceData.Length); |
149 | | - |
150 | | -// Vectorized copy from managed to unmanaged memory |
151 | | -arena.BulkSet(arrayHandle, sourceData); |
152 | | - |
153 | | -// --- 5. Unmanaged Strings (Save 50% RAM) --- |
154 | | -// Store text as UTF-8 in the Arena, bypassing C# UTF-16 overhead and the GC. |
155 | | -var stringHandle = arena.AllocateString("Hello from the Arena!"); |
156 | | - |
157 | | -// --- 6. Pointer-Speed Access via Spans --- |
158 | | -// Get a Span for zero-allocation, high-speed iteration. |
159 | | -// This is the "hot path" for game loops. |
160 | | -var span = arena.GetSpan<int>(arrayHandle, sourceData.Length); |
161 | | -foreach(ref var val in span) |
162 | | -{ |
163 | | - val *= 2; // Direct memory manipulation at CPU cache speeds |
164 | | -} |
165 | | - |
166 | | -// --- 7. Policy-Driven Maintenance --- |
167 | | -// Call this once per frame. It tracks the age of allocations. |
168 | | -arena.TickFrame(); |
169 | | - |
170 | | -// Periodically runs the Janitor (eviction) and Compaction (defragmentation) |
171 | | -// to ensure your FastLane never turns into "Swiss Cheese." |
172 | | -arena.RunMaintenanceCycle(); |
173 | | - |
174 | | -// --- 8. Manual Cleanup --- |
175 | | -arena.Free(healthHandle); |
176 | | -arena.Free(arrayHandle); |
177 | | -// Note: ArenaList and String handles are cleaned up similarly. |
| 70 | +using MemoryManager.Core; |
| 71 | + |
| 72 | +// --- PROFILE 1: Ultra-Fast Real-Time Game Loop --- |
| 73 | +// Initializes a high-frequency layout budget utilizing the |
| 74 | +// ultra-fast O(1) Linear Bump strategy with aggressive frame aging. |
| 75 | +var gameConfig = MemoryManagerConfig.CreateForGameLoop(totalBudget: 32 * 1024 * 1024); |
| 76 | +var gameArena = new MemoryArena(gameConfig); |
| 77 | + |
| 78 | +// --- PROFILE 2: Heavy I/O Stream / Asset Chunking --- |
| 79 | +// Instantiates a robust Free-List strategy engine capable of |
| 80 | +// managing out-of-order data transfers with larger block thresholds. |
| 81 | +var streamConfig = MemoryManagerConfig.CreateForBulkProcessing(totalBudget: 128 * 1024 * 1024); |
| 82 | +var streamArena = new MemoryArena(streamConfig); |
0 commit comments