Arena 内存管理
arena 是 upb 最核心的内存优化。每条解析出来的消息、每个字段、每次子分配都来自 arena。它的设计围绕一个原则:把常见分配压成几条指令,把释放摊销甚至消除。 本章拆解分配快路径、块的指数增长、arena 融合(fuse)和整块释放。
1. 双指针的”撞针”分配器
arena 的”当前区域”只有两个指针。公开的 struct upb_Arena 刻意做得极小——只有 ptr(下一个空闲字节)和 end(区域上界),外加一个可选的 sanitizer 成员(mem/internal/arena.h:37-41)。所有簿记(块链表、引用计数、fuse 链、分配器)都放在另一个 upb_ArenaInternal 体里,热路径完全不碰(mem/arena.c:55-117)。两者连续布局为 upb_ArenaState { upb_Arena head; upb_ArenaInternal body; }(mem/arena.c:120-123),由公开指针经简单指针运算找到内部体(upb_Arena_Internal,mem/arena.c:140-142)。
为什么这么切分:分配快路径只读写两指针的 head,编译器能把它们放进寄存器;而且它和 fuse/free 的原子操作处在不同的同步域里。源码注释明确指出 upb_Arena.ptr 不与 Fuse/Free/SpaceAllocated 同步,正是为了让分配路径不付任何原子代价(mem/arena.c:110-116)。
2. 分配快路径:头文件内联,几条指令
热路径 upb_Arena_Malloc 定义在头文件里(mem/internal/arena.h:73-90),直接内联进调用方:
size_t span = _upb_Arena_AllocSpan(size); // 对齐 + guard
if (UPB_UNLIKELY(_upb_ArenaHas(a) < span)) { // 一次边界比较
return _upb_Arena_SlowMalloc(a, span); // 走 out-of-line 慢路径
}
void* ret = a->ptr;
a->ptr += span; // 指针前移
return ...;
整条快路径就是:算 span、和 end-ptr 比一次、返回 ptr、推进 ptr。没有函数调用、没有原子、常见情况下唯一的分支不被走(UPB_UNLIKELY 把慢路径偏置出去)。慢路径 _upb_Arena_SlowMalloc 在内联函数内部本地前置声明(mem/internal/arena.h:79),定义在 mem/arena.c:479。
对齐:UPB_MALLOC_ALIGN 普通为 8、HWASAN 下 16(port/def.inc:179、:182)。每个 span = UPB_ALIGN_MALLOC(size) + kUpb_Asan_GuardSize(mem/internal/arena.h:58-60),所以推进 ptr 后 ret 和新 ptr 都保持对齐(断言在 :86-87)。guard 在 ASAN 下为 32、否则为 0(port/sanitizers.h:51、:53),等于在插桩构建里免费获得分配间的红区。_upb_ArenaHas(当前区域剩余字节)就是 end - ptr(mem/internal/arena.h:54-56)。
头文件里还有几个复用同一组区域指针的零成本操作:upb_Arena_ShrinkLast 缩回最近一次分配(arena.h:92-112)、upb_Arena_TryExtend 原地扩展(arena.h:114-130)、upb_Arena_Realloc 二者结合、只有不得不挪动时才拷贝(arena.h:132-161)。这些被数组/未知字段的原地增长大量利用(见 第 6、第 8 章)。
3. 块的指数增长(慢路径)
当前区域用尽时走 _upb_Arena_SlowMalloc(mem/arena.c:479-508):
- 用
_upb_Arena_NextBlockSize算下一块大小(mem/arena.c:436-461),采用指数增长:min(last_block_size * 2, max_block_size)(:443)。超大的一次性请求绕过几何级数、单独配一块刚好大小(:445-457);size_hint跟踪这类离群请求是否常见到值得纳入常规方案(:463-475)。 - 分配块(
_upb_Arena_AllocBlock,:385-396),前插到块链表(_upb_Arena_AddBlock,:398-413),装为新区域(_upb_Arena_UseBlock,:415-425)。 - 然后尾递归回内联快路径
upb_Arena_Malloc真正切出字节(:506)。
每块前缀一个 upb_MemBlock { next; size; } 头(mem/arena.c:36-42),可用内存从 kUpb_MemblockReserve(对齐后的 sizeof(upb_MemBlock))字节处开始(:130-131、:382)。默认最大块大小见 UPB_DEFAULT_MAX_BLOCK_SIZE(port/def.inc:382、:384),可由 SetMaxBlockSize 调整(mem/arena.c:29-34)。
指数增长的意义和 std::vector 一样:N 次分配总共只触发 O(log N) 次真正的 malloc,单次分配摊销成常数。
4. 初始块 / SOO 优化
upb_Arena_Init(mem, n, alloc) 允许调用方把一段栈上/静态缓冲当作第一块传进来(mem/arena.h:41-50、mem/arena.c:550-592)。当 mem 足够大时,upb_ArenaState 直接构造在用户缓冲里(:570-587):ptr 从状态之后(对齐)开始、end 指到缓冲末尾,于是最初的若干次分配零堆调用。block_alloc 被打上 has_initial = 1 标记(:584)。没有缓冲(或太小)时走 _upb_Arena_InitSlow malloc 第一块(:510-548)。
解码器正是这个优化的大客户:它用 UPB_ARENA_SIZE_HACK 在不暴露完整结构的前提下内联一个 arena 大小的缓冲(mem/internal/arena.h:20-33)。
注意:带初始块的 arena 不能被 fuse 或 ref——它的缓冲生命周期归用户所有、无法被延长(守卫见 mem/arena.c:884-889、:920、:950)。
5. arena 融合(fuse):零拷贝共享生命周期
融合让跨 arena 的引用安全:两个互相引用的 arena 合并生命周期,谁都不会在对方还活着时被释放,且不拷贝任何数据。这是”把子消息零拷贝挂到另一棵消息树”的基础。
模型是一棵带引用计数的 union-find(并查集)森林。每个 arena 的 parent_or_count 是一个 tagged 字(mem/arena.c:92-95):低位 1 表示”我是根”、持有整组的引用计数(左移一位);低位 0 表示”指向父节点”。tag 辅助函数在 :144-205。
找根 _upb_Arena_FindRoot(mem/arena.c:256-286)沿父指针走到顶,途中做路径分裂——把每个节点的祖父写回它的父槽(:281)以压平树。已是根的情况只需一次 relaxed load 直接返回(:257-263);只有多级情况才付 acquire load(:267),这是一处针对 ARM 的有意优化(:264-266)。
upb_Arena_Fuse(mem/arena.c:874-902):自融合直接返回(:875);任一带初始块则拒绝(:884-889);否则循环调用 _upb_Arena_DoFuse + _upb_Arena_FixupRefs 直到成功,竞争失败就重试(:893-901)。
原子核心 _upb_Arena_DoFuse(mem/arena.c:785-846)几处关键设计:
- 两根相同→已融合(:795-798)。
- 总是融合进地址更小的那个根,防止环 / 活锁(:803-808)。
- 必须先把 r2 的引用计数转移进 r1、再改父指针——因为一旦 r1 成为父,竞争的 free 就会开始减 r1 的计数(:810-822)。第一次 CAS 把 r2 的引用加到 r1(:825-829),第二次 CAS 把 r2 从”计数”换成”指向 r1 的 tagged 父指针”(:833-840);若失败,多加给 r1 的引用记进
ref_delta待后续清除。
整个融合是无锁的;融合 N 个 arena 因路径分裂而近乎扁平;而且不拷贝任何消息数据——只是把生命周期链在一起。upb_Arena_IsFused 比较两根、对并发重指做重试(mem/arena.c:904-916)。
6. 释放:整块一次性归还
这是 arena 的招牌收益:没有逐对象 free。upb_Arena_Malloc 没有配对的 free,整个 arena 一遍释放。
upb_Arena_Free(mem/arena.c:631-665)先走到根;引用计数优化:若根计数恰为 1,跳过昂贵的读-改-写直接释放(:646-652),注释解释只有别的线程需要观测到变化时才用 RMW(:643-645);否则做 release/acquire 的 CompareExchangeWeak 减一、竞争重试(:654-664)。
真正的批量释放 _upb_Arena_DoFree(mem/arena.c:594-629):沿融合链表遍历每个 arena(:596、:627),对每个再走 blocks 链表(:609-623),要么把块用 upb_free_sized 还给分配器(:620),要么(块其实是个 upb_ArenaRef、size==0 时)对被引用 arena 减一引用(:614-618)。所有块释放完后调用分配器的 cleanup/finalizer(:624-626,由 upb_Arena_SetAllocCleanup 设置,:777-782)。
为什么快:释放上千个对象只是遍历一条很短的块链表、调几次 free——是 O(块数) 而非 O(对象数),也没有析构遍历对象图。
7. 线程模型、原子与分配器抽象
- 线程模型(mem/arena.h:8-19):
upb_Arena的分配不是线程安全的(单线程设计,正因如此分配路径不需要原子);只有生命周期操作(Fuse / IsFused / IncRefFor / DecRefFor / SpaceAllocated / Free)线程安全。 - 原子:所有共享状态(
parent_or_count、next、previous_or_tail、space_allocated)都包在UPB_ATOMIC(T)里。代码对内存序极其讲究:根计数快路径 relaxed(mem/arena.c:257)、向父遍历 acquire(:267)、减一 release(:657)、space_allocatedrelaxed(:407-409)。_upb_Arena_AddBlock故意用 load/add/store 而非原子 fetch-add,因为线程从不竞争块分配、且 fetch-add 在 v8.1 之前的 ARM 上更慢(:402-409)。 - 分配器抽象:
upb_alloc是一个把 malloc/free/realloc 合一的单字段 vtable(mem/alloc.h:20-40);upb_SizeReturningMalloc会汇报分配器的实际块大小,让 arena 用上多出来的空隙(mem/alloc.h:52-59,arena 在 mem/arena.c:380-381 消费)。默认全局分配器upb_alloc_global是对 libcrealloc/free的薄封装(mem/alloc.c:15-28)。
C++ 包装 upb::Arena 就是一个 RAII unique_ptr<upb_Arena, upb_Arena_Free>,转发 Fuse/IsFused/RefArena(mem/arena.hpp:21-47)。
8. 本章小结
| 机制 | 出处 |
|---|---|
| 公开 arena = ptr/end 双指针 | mem/internal/arena.h:37-41 |
内联分配快路径 upb_Arena_Malloc | mem/internal/arena.h:73-90 |
_upb_Arena_AllocSpan = 对齐 + guard | mem/internal/arena.h:58-60 |
| ShrinkLast / TryExtend / Realloc | mem/internal/arena.h:92-161 |
upb_ArenaInternal 簿记字段 | mem/arena.c:55-117 |
| ptr 不与 fuse/free 同步(注释) | mem/arena.c:110-116 |
_upb_Arena_NextBlockSize 指数增长 | mem/arena.c:436-461 |
_upb_Arena_SlowMalloc + 尾递归 | mem/arena.c:479-508 |
upb_Arena_Init 初始块/SOO | mem/arena.c:550-592 |
UPB_ARENA_SIZE_HACK | mem/internal/arena.h:20-33 |
_upb_Arena_FindRoot + 路径分裂 | mem/arena.c:256-286 |
_upb_Arena_DoFuse(两次 CAS) | mem/arena.c:785-846 |
upb_Arena_Fuse + 初始块守卫 | mem/arena.c:874-902 |
_upb_Arena_DoFree(块链表遍历) | mem/arena.c:594-629 |
upb_Arena_Free 引用计数快路径 | mem/arena.c:631-665 |
| 线程模型 | mem/arena.h:8-19 |
AddBlock 用 load/add/store 而非 fetch-add | mem/arena.c:402-409 |
upb_alloc vtable / 全局分配器 | mem/alloc.h:20-40、mem/alloc.c:15-28 |
C++ upb::Arena RAII | mem/arena.hpp:21-47 |