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_Internalmem/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_GuardSizemem/internal/arena.h:58-60),所以推进 ptrret 和新 ptr 都保持对齐(断言在 :86-87)。guard 在 ASAN 下为 32、否则为 0(port/sanitizers.h:51:53),等于在插桩构建里免费获得分配间的红区。_upb_ArenaHas(当前区域剩余字节)就是 end - ptrmem/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_SlowMallocmem/arena.c:479-508):

  1. _upb_Arena_NextBlockSize 算下一块大小(mem/arena.c:436-461),采用指数增长min(last_block_size * 2, max_block_size):443)。超大的一次性请求绕过几何级数、单独配一块刚好大小(:445-457);size_hint 跟踪这类离群请求是否常见到值得纳入常规方案(:463-475)。
  2. 分配块(_upb_Arena_AllocBlock:385-396),前插到块链表(_upb_Arena_AddBlock:398-413),装为新区域(_upb_Arena_UseBlock:415-425)。
  3. 然后尾递归回内联快路径 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_SIZEport/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-50mem/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_FindRootmem/arena.c:256-286)沿父指针走到顶,途中做路径分裂——把每个节点的祖父写回它的父槽(:281)以压平树。已是根的情况只需一次 relaxed load 直接返回(:257-263);只有多级情况才付 acquire load(:267),这是一处针对 ARM 的有意优化(:264-266)。

upb_Arena_Fusemem/arena.c:874-902):自融合直接返回(:875);任一带初始块则拒绝(:884-889);否则循环调用 _upb_Arena_DoFuse + _upb_Arena_FixupRefs 直到成功,竞争失败就重试(:893-901)。

原子核心 _upb_Arena_DoFusemem/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 的招牌收益:没有逐对象 freeupb_Arena_Malloc 没有配对的 free,整个 arena 一遍释放。

upb_Arena_Freemem/arena.c:631-665)先走到根;引用计数优化:若根计数恰为 1,跳过昂贵的读-改-写直接释放(:646-652),注释解释只有别的线程需要观测到变化时才用 RMW(:643-645);否则做 release/acquire 的 CompareExchangeWeak 减一、竞争重试(:654-664)。

真正的批量释放 _upb_Arena_DoFreemem/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_countnextprevious_or_tailspace_allocated)都包在 UPB_ATOMIC(T) 里。代码对内存序极其讲究:根计数快路径 relaxed(mem/arena.c:257)、向父遍历 acquire(:267)、减一 release(:657)、space_allocated relaxed(: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 是对 libc realloc/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-40mem/alloc.c:15-28
C++ upb::Arena RAII mem/arena.hpp:21-47

本站访问量

This site uses Just the Docs, a documentation theme for Jekyll.