原生内存与对象池使用指南(AlignedMemoryManager / NativeArena / NodeArena / PinnedBufferPool / OverflowPool)
给谁看:需要非托管内存/池化的组件开发者(引擎热路径、对齐缓冲、批量临时区)。 回答什么:五种内存积木怎么选、怎么借怎么还、什么不要做。 ⚠️ 铁律(先记住):非托管内存 GC 不管——自己 new 的要 Dispose,池里借的要归还, 两种都不做 = 真泄漏。完整积木全景见
../COORDINATION.md。
0. 选型(需求 → 用哪个)
| 需求 | 用 | 线程安全 | 一句话 |
|---|---|---|---|
| 单块对齐原生内存(O_DIRECT/DMA/16B CAS 背板) | AlignedMemoryManager |
否(单块单线程读写) | new(size, alignment),挂到对象即用,Dispose 释放 |
| 短生命周期 bump 分配(批量/临时缓冲) | NativeArena |
否(单线程,建议 ThreadStatic) | 一次分配大块,线性 bump,Reset 复用 |
| 长命变长节点驻留(并发分配、指针恒稳) | NodeArena |
Alloc 是(并发安全)/ Dispose 归结构层收口 | 4MB 块 CAS-bump,只增不减,无需逐节点释放 |
| pinned/对齐 buffer 池(高频 Rent/Return) | PinnedBufferPool |
是(全局池多线程安全) | 分桶 + thread-local 栈,热路径零分配 |
| 轻量固定容量对象池(非内存对象) | OverflowPool<T> |
是(多线程安全) | ConcurrentQueue,满则 disposer 回收 |
1. 借还规则(最容易错的地方)
| 来源 | 归还方式 | 说错就错 |
|---|---|---|
自己 new AlignedMemoryManager(...) |
Dispose() |
不 Dispose = 真泄漏(无 finalizer) |
pool.Rent(size) → byte[] |
pool.Return(buf) |
别 Dispose——归还即复用 |
pool.RentAligned(...) → AlignedMemoryManager |
pool.ReturnAligned(mem) |
不是 Dispose(池 Reset 复用);带归属池校验,别交叉归还 |
原文曾有"RentAligned 产物必须 Dispose"的说法——那是错的:池产物归还是
ReturnAligned, Dispose 会绕过池的复用协议。借了要还、建了要 Dispose,两条路都不可省。
2. AlignedMemoryManager —— 单块对齐原生内存
using var mem = new AlignedMemoryManager(size: 4096, alignment: AlignmentConst.Alignment4K);
Span<byte> span = mem.GetSpan(); // 含校验(对外接口用)
ref Header h = ref mem.GetRef<Header>(offset: 0); // 强类型引用
Span<byte> fast = mem.GetSpanUnsafe(0, 64); // 热路径零校验(调用方自证合法)
unsafe { byte* p = mem.BytePtr; } // 原生指针(O_DIRECT pread/pwrite)
- 继承
MemoryManager<byte>——可给ReadAsync(Memory<byte>)。 - ⚠️
lockPhysicalMemory=true:Linux 需CAP_IPC_LOCK或调高RLIMIT_MEMLOCK;alignment须 ≥ 系统页大小。 - ⚠️ 无 finalizer——不 Dispose 即真泄漏。
3. NativeArena —— 线性 bump 分配
using var arena = new NativeArena(size: 1 << 20); // 一次分配整块
Span<int> nums = arena.Allocate<int>(count: 1024); // 线性 bump(只前进,无单块 Free)
arena.Reset(); // offset 归零复用(内存不释放)
- 适合批量/临时缓冲;多线程并发
Allocate须外部同步。 - 有 finalizer 兜底但不保证时效——仍应主动 Dispose。
4. NodeArena —— 并发安全的变长节点驻留竞技场
using var arena = new NodeArena(); // 4MB 块起步(ChunkSize = 1 << 22)
Node* node;
unsafe { node = (Node*)arena.Alloc(sizeof(Node)); } // 8 对齐分配,返回恒稳裸指针
// 填充节点字段后把指针缓存进上层映射表——跨块增长长命有效
- 指针恒稳:块只增不减、永不重分配搬移——缓存进上层结构的裸节点指针在 arena 持续增长时 长命有效(SkipList 节点驻留形态的基座)。
- 并发 Alloc 安全:4MB 块内
Interlockedbump(无锁快路径,读者侧 admit 也安全); 块满建新块(lock 双检)。 - 无逐节点释放:块内存生命周期 = arena 生命周期(
Dispose全释放)——配合"节点缓存不逐出" 的索引形态;并发竞争的重复分配 = 有界字节浪费,非泄漏。 - 单写者 + 并发读者契约:Alloc 可并发;Dispose 由结构层 Resources 收口(确定性先于读者终结)。
- 与
NativeArena分野:NativeArena = 单线程线性 bump +Reset复用(批量临时缓冲); NodeArena = 并发分配 + 只增不减 + 指针恒稳(长命驻留对象)。
5. PinnedBufferPool —— pinned/对齐 buffer 池(旗舰池)
using var pool = new PinnedBufferPool(maxPerBucket: 64);
// pinned byte[] 池
byte[] buf = pool.Rent(size: 4097, zeroMemory: false); // size 向上取整 2 的幂(4097→8192)
try { /* 用 buf */ }
finally { pool.Return(buf); } // 归还(不是 Dispose)
// 对齐原生内存池
AlignedMemoryManager mem = pool.RentAligned(size: 4096, alignment: AlignmentConst.Alignment4K);
try { Span<byte> s = mem.GetSpan(); /* 用 */ }
finally { pool.ReturnAligned(mem); } // 归还(池 Reset 复用,非 Dispose)
- size 向上取整到 2 的幂(最多 50% padding)——别指望精确大小。
- 别和
ArrayPool<byte>.Shared混用——归还非本池 buffer 会被静默忽略/Dispose。 ReturnAligned带池身份校验:归还非本池产出的会被拒——别交叉归还。pool.Dispose()释放所有 buffer(全局栈 + 各 thread-local 栈)。
6. OverflowPool<T> —— 轻量固定容量对象池
using var pool = new OverflowPool<MyContext>(size: 32, disposer: ctx => ctx.Cleanup());
if (pool.TryGet(out MyContext? ctx)) // 借:命中 true / 池空 false(自行 new)
ctx.Reuse();
else
ctx = new MyContext();
pool.TryAdd(ctx); // 还:未满入池 true / 满→disposer 回收 false
- 容量是软约束(高并发瞬时可能超几个,无正确性影响)。
- disposer 默认 no-op——对象若持有非托管资源必须传回收回调,否则池满丢弃即泄漏。
7. 决策速查
我要单块对齐内存(O_DIRECT / CAS 背板)?
→ new AlignedMemoryManager(size, alignment)。using / Resources。热路径 GetSpanUnsafe。
我要批量临时缓冲(用完整体丢)?
→ new NativeArena(size)。Allocate/AllocateBytes,用完 Reset 复用或 Dispose。
我要长命驻留的变长节点(并发分配、指针永不搬移)?
→ new NodeArena()。Alloc(size) 得恒稳裸指针;不逐节点释放,Dispose 由结构层收口。
我要高频 Rent/Return 复用 buffer?
→ PinnedBufferPool。Rent/Return(pinned 数组)或 RentAligned/ReturnAligned(对齐原生内存)。
size 取整 2 幂;别跨池归还;Dispose 释放全部。
我要复用非内存对象(会话/上下文)?
→ OverflowPool<T>(size, disposer)。TryGet/TryAdd;满则 disposer 回收。