arraypool 仅在短生命周期、固定尺寸、高频复用三者同时满足时才提升性能;否则 rent 会加剧 gc 压力,且必须严格遵循租还同线程、正确归还、按实际长度使用等约束。

ArrayPool<t></t> 不是“用了就快”,而是“租了不还、用错尺寸、跨线程乱传,就直接拖垮性能”。它只在**短生命周期 + 固定尺寸 + 高频复用**三者同时成立时才真正生效;否则你写的每行 Rent 都在悄悄给 GC 加压。
rent 返回的数组长度为什么总是大于你要的 minimumLength
因为 ArrayPool<t>.Shared</t> 内部按 2 的幂预建桶(128、256、512、1024、2048…),调用 Rent(800) 实际返回的是长度为 1024 的 byte[]。这不是 bug,是设计取舍:减少碎片、加速匹配。
- 你必须用
buffer.Length判断可用空间,不能硬写buffer[0..800]—— 越界写入不会报错,但会污染后续租用者的内存 - 如果只用前 N 字节,归还时建议显式传
clearArray: true,尤其处理协议头、token、密码等敏感数据 -
Rent(1)是典型浪费:池中最小桶通常是 256,你拿 1 字节,实际占 256,且无法被其他小尺寸请求复用
Shared 池够用吗?什么情况下必须用 ArrayPool.Create
ArrayPool<byte>.Shared</byte> 是进程级静态单例,适合通用场景;但它不隔离、不清除、不限容,高并发或混合负载下容易互相干扰。
- 日志模块突发写入大量小数组(如
Rent(256)),可能耗尽桶容量,导致网络模块Rent(4096)只能 new —— 这不是理论,是压测中实测现象 - 需要限制最大数组尺寸(比如你业务永远不需要 >64KB 缓冲区),就得用
ArrayPool.Create(minLength: 256, maxLength: 65536),否则池内会囤积巨型数组 - 要定制清理策略(比如某些场景可接受不清零省开销),只能通过
Create(clearMode: ArrayClearMode.Never)控制,默认是Always
Return 调用失败或遗漏的后果很具体
Return 不是“可有可无”的收尾动作,它是池能否持续工作的唯一阀门。
- 漏调
Return→ 池中可用数组持续减少 → 后续Rent被迫 new → GC 压力回升,池化失效 - 提前
Return(如在await后续代码还没执行完时就归还)→ 数组可能被立刻复用 → 两个逻辑读写同一块内存 → 数据错乱、协议解析失败、偶发IndexOutOfRangeException - 归还一个被
Array.Resize过的数组,或子数组(如buffer.AsSpan(0, 512).ToArray()),Return会静默失败 —— 不报错、不归池、不警告,只默默丢弃
高并发下租借数组不能跨线程共享
ArrayPool<t>.Shared</t> 只保证 Return 操作线程安全,不保证你租出的那块数组能被多个线程同时读写。这是最常被忽略的底层约束。
- 错误模式:
Task.Run(() => { var b = Rent(1024); Write(b); })—— 租在主线程,写在后台线程,Return却在主线程调,中间引用已失控 - 正确做法:租、用、还尽量在同一个同步上下文或明确的异步作用域内完成;若必须跨线程,需加锁或改用
MemoryPool<t></t>+IMemoryOwner<t></t>管理所有权 - 不要把租来的数组塞进静态集合、缓存字典或事件参数里长期持有 —— 它的生命周期由你控制,不是由 GC 或池管理
Rent 和 Return,而在于判断哪一段逻辑**确实满足毫秒级生命周期、固定尺寸、高频复用**——多数人卡在这一步,却直接抄代码开干。










