优化技术总览
前面各章沿”模块”纵向拆解。本章横向收口:把贯穿全库、反复出现的优化手法归类汇总,每条都给出在各模块里的落点。这既是复习,也是一份”如何写又快又小的 C 库”的实战清单。
1. 表驱动:把决策上移到构建期
upb 最根本的思路是运行时不做反射、只查表。schema 在生成时就被压成静态表,运行时只索引 + 搬数据。
- MiniTable 把整个消息布局压成一个紧凑只读结构 + 12 字节字段数组(mini_table/internal/message.h:71-94、field.h:21-35),解析/序列化/访问全部查它(第 5 章)。
- fasttable 在建表时就把”每个 tag 走哪个解析函数”定死(wire/decode_fast/select.c:227),运行时一次掩码索引就跳过去(第 7 章)。
- wire op 表(
kVarintOps/kDelimitedOps,wire/decode.c:763、:810)把 (字段类型, wire 类型) 映射成一个小整数 op,免去层层 if。
2. 尾调用 + 寄存器传参:消灭栈流量
fasttable 的核心(第 7 章):UPB_MUSTTAIL + UPB_PRESERVE_NONE(port/def.inc:434、:446)把解析过程编译成一串寄存器传参的尾跳转。7 个解析参数(含一路在寄存器里累积的 hasbits)全程不落栈,解析器之间转移就是 jmp。hasbits 只在消息结束/重复字段前才刷回内存(decode_fast/dispatch.h:88-92)。
3. 内存:一次大开销换无数次小开销
- Arena bump 分配(第 4 章):分配快路径几条指令、无原子(mem/internal/arena.h:73-90);释放整块一次性归还、O(块数) 而非 O(对象数)(mem/arena.c:594-629)。
- 块指数增长(mem/arena.c:436-461)与数组容量倍增(message/array.c:160-185):N 次增长摊销成常数。
- arena 融合(mem/arena.c:785-846):用无锁 union-find 把生命周期链在一起,零拷贝共享子消息。
- 原地扩展
TryExtend(mem/internal/arena.h:114-130):数组增长、未知字段追加都尽量原地扩,避免拷贝。 - 解码器 arena swap-in(wire/internal/decoder.h:114-127):把热的分配字段与解码器其他热字段排在一起,改善局部性。
4. 摊销与单遍:把”每次检查”变成”每批检查”
- epsilon-copy 输入流(第 7 章):16 字节 slop + patch 缓冲,把逐字节边界检查摊销成每字段一次(wire/internal/eps_copy_input_stream.h:25-33、:170-183)。
- 反向写入编码器(第 8 章):从尾部往前写,长度前缀确定后再前置,序列化严格一遍完成(wire/internal/encoder.c:8、:434-450)。
- 定长 packed 单次 memcpy(wire/internal/encoder.c:336-367):小端下内存布局已等于 wire 布局,整段一次拷。
5. 零拷贝与别名
- 字符串别名:
AliasString选项下解析出的字符串直接指向输入缓冲,不拷贝(wire/internal/decoder.h:244-263)。 - 未知字段别名 + 撞针合并:未知字节可别名进输入;与上一段紧邻时只
size += len(message/internal/message.h:256-284)。 - 编码器从 arena 偷空间:小消息序列化不触发独立 malloc(wire/internal/back_alloc.c:94-109)。
6. 位压缩:让数据结构更小、cache 更友好
upb_MiniTableField.mode一个字节装下 FieldMode + LabelFlags + Rep(field.h:33-73);presence一个 int16 三义(hasbit / oneof / 无)(field.h:24)。upb_Array.data是 tagged 指针:低位装元素大小 lg2 + frozen(message/internal/array.h:30-65)。upb_Message仅 8 字节、低位作 frozen(message/internal/types.h:18-48);扩展/未知共用一条 tagged 指针数组(message/internal/message.h:45-156)。- 哈希表
next字段用 0/1 当空槽/链尾哨兵,省掉额外标志位(hash/common.h:151-159)。 - MiniDescriptor base92 把整张布局压成一个短可打印字符串(mini_descriptor/internal/base92.h:34-55)。
7. “分支折叠”:通用代码、特化性能
访问器只有一族由 MiniTableField 参数化的通用 getter/setter,看着分支重重,但生成代码传入编译期常量描述符 + UPB_ASSUME,编译器把分支全部常量折叠,留下直线代码(注释 message/internal/accessors.h:215-245)。一份源码同时服务生成访问器、反射、MiniTable 访问器——不为通用性付运行时代价。
8. 把冷路径踢出热路径
UPB_NOINLINE标记回落/冷函数(长 varint wire/reader.c:19-31、fasttable done 回落 decode_fast/dispatch.c:20-51),让热函数体积小、留在 i-cache。UPB_LIKELY/UPB_UNLIKELY把罕见分支偏置出去(port/def.inc:265-269);UPB_UNPREDICTABLE在二分查找里反过来压制预测(mini_table/internal/message.h:168)。- longjmp 错误处理(base/error_handler.h:62-76):热路径无逐层
if(err) return,出错才一跳到底。
9. 平台特化:抠到单条指令
- aarch64 调优的字段二分查找,用内联汇编 launder 屏障控制指令选择(mini_table/internal/message.h:181-263)。
- arm64 手写清零汇编,避免大量小 memset 的 PLT 调用与寄存器溢出(message/internal/message.h:165-216)。
- ARM64 手写 varint 编码汇编,用
clz+ 跳转表(wire/internal/encoder.c:155-240)。 UPB_SIZE(s32,s64)让同一份生成代码在 32/64 位 ABI 都正确(port/def.inc:107-111)。
10. 为”小”服务的优化(代码体积)
“快”之外,upb 同样在抠体积:
- 快核心 / 反射切分:生成消息仅
{ upb_Message base; }、只依赖 MiniTable,反射可整段被链接器裁掉(第 2 章、README.md:34-36)。 - weak alias + 占位 MiniTable:让未引用的表被丢弃(port/def.inc:695-754)。
- 无全局状态 / 链接器数组:用
UPB_LINKARR_*替代 main 前注册(port/def.inc:547-650)。 - fasttable 可选 + 优雅降级:不支持的平台
UPB_FASTTABLE_INIT/MASK展开为空,生成代码无需两份(port/def.inc:500-506)。 - amalgamation:合并成单文件随语言扩展分发(第 3 章)。
11. 一张总表
| 优化手法 | 代表落点 | 章节 |
|---|---|---|
| 表驱动(MiniTable / fasttable / op 表) | message.h:71-94 | 5, 7 |
| 尾调用 + 寄存器传参 | decode_fast/dispatch.h:32-64 | 7 |
| Arena bump 分配 + 整块释放 | mem/internal/arena.h:73-90 | 4 |
| 指数增长 / 容量倍增 | mem/arena.c:436-461 | 4, 8 |
| arena 融合(零拷贝共享) | mem/arena.c:785-846 | 4 |
| epsilon-copy 摊销边界检查 | eps_copy_input_stream.h:25-33 | 7 |
| 反向写入单遍编码 | encoder.c:434-450 | 8 |
| 零拷贝别名(字符串/未知字段) | decoder.h:244-263 | 6, 7 |
| 位压缩(mode / tagged 指针) | field.h:33-73 | 5, 6, 8 |
| 分支折叠的通用访问器 | accessors.h:215-245 | 6 |
| 冷路径外提 + longjmp | base/error_handler.h:62-76 | 7 |
| 平台特化汇编 | message.h:181-263 | 5, 6, 8 |
| 快核心/反射切分(为体积) | README.md:34-36 | 2, 3 |
结语
upb 没有单一的”银弹”。它的快与小,来自把表驱动、尾调用、arena、摊销、零拷贝、位压缩、分支折叠、平台特化这一整套手法系统性地、彼此配合地用到每一条热路径上。理解这些手法,不仅是读懂 upb,也是一份可迁移到其他高性能 C/C++ 系统的工程经验。