整体架构
本章建立 upb 的全局坐标系:它分成哪些层、各层如何依赖、”快核心”与”反射层”为何要切开,以及一次解析 / 序列化在这些层之间是怎么流动的。
1. 分层总览
upb 的目录基本就是它的分层。依赖关系自下而上:底部是与 protobuf 概念无关的移植 / 内存 / 基础设施层,往上逐渐变成 protobuf 专属逻辑,顶部是最重的反射层。
┌──────────────────────────────────────────────┐
顶层 │ util def_to_proto / required_fields │ 基于反射的高层工具
├──────────────────────────────────────────────┤
反射 │ reflection DefPool / *_def / descriptors │ 重、可裁剪
├──────────────────────────────────────────────┤
│ json JSON 编解码 │
格式 │ text 文本序列化 / debug_string │ 依赖 reflection + lex
├──────────────────────────────────────────────┤
│ wire decode / encode / decode_fast │ 二进制编解码(含 fasttable)
├──────────────────────────────────────────────┤
快 │ message 消息表示 / 访问器 / array / map │ in-memory 表示
核心 │ mini_table upb_MiniTable 运行时布局表 │ 表驱动的核心
│ mini_descriptor 从紧凑串重建 MiniTable │
├──────────────────────────────────────────────┤
│ hash str_table / int_table 哈希表 │
基础 │ lex 数字 / unicode 转换 │
设施 │ base status / 常量 / string_view │
│ mem arena / alloc ← 内存模型核心 │
├──────────────────────────────────────────────┤
移植 │ port def.inc / undef.inc / atomic │ 纯预处理器,无运行时代码
└──────────────────────────────────────────────┘
各层职责简述:
- port(port/def.inc、
undef.inc、atomic.h、sanitizers.h)——纯预处理器移植/优化层,没有任何运行时代码。每个翻译单元最后都包含它,每个头文件结尾再包含undef.inc把宏清干净(见 第 3 章)。所有always_inline、分支提示、对齐、musttail检测都集中在这里。 - mem(mem/arena.c、
alloc.c)——arena 分配器,整个库的内存模型核心。arena 的分配不是线程安全的(单线程设计,所以分配路径无需原子),但其生命周期操作(fuse/refcount)是线程安全的(mem/arena.h:13-18)。 - base——status、descriptor 常量、
upb_StringView等所有层共用的基础类型。 - hash——开放寻址哈希表,被反射和扩展查找复用。
- lex——JSON 和文本格式用到的数字/unicode 转换。
- mini_table——运行时”快核心”的数据结构。
struct upb_MiniTable(mini_table/internal/message.h:71-94)和struct upb_MiniTableField(mini_table/internal/field.h:21-35)紧凑地描述一个消息的内存布局,不依赖反射就能驱动解析/序列化。 - mini_descriptor——在运行时从紧凑的 “MiniDescriptor” 编码重建 MiniTable,供动态语言按需构造布局。
- message——in-memory 消息表示与字段访问器,外加
upb_Array/upb_Map容器。 - wire——二进制 wire 格式的解析/序列化;
wire/decode_fast/是可选的 fasttable 解析器。 - json / text——JSON 与文本格式,依赖
lex与reflection。 - reflection——重量级层:
upb_DefPool(reflection/def_pool.h:28-40)加上upb_MessageDef、upb_FieldDef等完整 descriptor 对象。 - util——基于反射的高层工具。
每个头文件都遵循 “Must be last” 约定:在末尾包含 port 层,例如 mem/arena.h:30-31。这保证 port 层的宏在所有声明之后才生效、且不会泄漏给用户。
2. 核心切分:快核心 vs 反射层
理解 upb 体积优势的关键,是它把 MiniTable 驱动的快核心(mini_table + message + wire)与反射层(DefPool / descriptors)彻底切开。
生成代码只依赖快核心:
- 生成的
.upb.h里,一个消息结构体仅仅是{ upb_Message base; }(见 descriptor.upb.h:21-23)。 - 生成的
.upb_minitable.c发射的是静态的upb_MiniTable实例(见 descriptor.upb_minitable.c:29-39)。
因为解析/序列化只需要 MiniTable,反射就成了可选、可被 tree-shaking 掉的部分:如果一个程序从不链接 DefPool,所有 descriptor 机器都不会被拉进来。这正是 README “optional reflection” 那句话背后的机制(README.md:34-36)。
为了让”裁掉未用的 MiniTable”也能成立,port 层还提供了 weak alias + 占位 MiniTable 的机制(port/def.inc:695-754),让链接器能丢弃没被引用的表。
生成代码 (.upb.h + .upb_minitable.c)
│ 只依赖
▼
┌──────────── 快核心 ────────────┐ ┌──── 反射层(可选/可裁剪)────┐
│ mini_table message wire │ ······ │ reflection: DefPool / Defs │
│ 表驱动、无全局状态、零反射依赖 │ 按需 │ 完整 descriptor、名字查找等 │
└────────────────────────────────┘ 链接 └──────────────────────────────┘
这条切分线是 upb 一切”小”的来源;后续 第 5 章 会深入 MiniTable 本身。
3. 字段查找:快核心也在抠指令
即便是一次”按字段号找字段”,upb 也做了多档优化。upb_MiniTable_FindFieldByNumber(mini_table/internal/message.h:283-309):
- dense 快路径:因为字段通常按字段号升序排列,凡是字段号正好是
1,2,3,…的前缀都能直接用数组下标定位。dense_below记录这段稠密前缀的长度;若number-1 < dense_below,一次比较 + 一次索引取出(message.h:288-292),不查找。 - 稀疏回落:否则在
[dense_below, field_count)上二分查找(message.h:304-308)。
二分查找甚至有两套实现:通用版(message.h:161-176)和一套手工调优的 aarch64 版,用内联汇编 “launder” 屏障控制小核上的指令选择(message.h:181-263)。这种”抠到单条指令”的程度,是理解 upb 性能态度的一个缩影。
4. 一次解析的生命周期
把各层串起来,看一条二进制消息从字节变成内存对象的全过程:
upb_Decode(buf, size, msg, minitable, extreg, options, arena)
│ wire/decode.c:1224
▼
upb_Decoder_Decode ← setjmp 建立错误落点 (decode.c:1201)
│
▼
_upb_Decoder_DecodeMessage ← 每条消息一个循环 (decode.c:1169)
│
├─[fasttable 可用?]─► 尾调用快速解码器 (decode_fast/dispatch.h)
│ 16位tag → fasttable → musttail 解析函数链
│ 命中不了的字段无缝回落 ↓
▼
_upb_Decoder_DecodeFieldNoFast ← 表驱动主解码器 (decode.c:1042)
│ 读 tag → MiniTable 查字段 → 按 wire 类型分派
├─► 标量:写入 msg + field.offset
├─► 子消息:压限制、递归(深度受限)
├─► packed 重复:批量读
└─► 未知字段:捕获原始字节,可零拷贝别名进输入缓冲
│
所有内存都从 arena 撞指针分配(mem/arena.c)
涉及的层:wire(主控)→ mini_table(查字段)→ message(写入内存表示、扩展/未知字段)→ mem(分配)。错误(OOM、格式错误、超深、非法 UTF-8)通过 longjmp 一跳回到 upb_Decoder_Decode 的 setjmp,热路径上没有逐层的错误返回检查。详见 第 7 章。
5. 一次序列化的生命周期
序列化方向相反,而且从缓冲区尾部往前写:
upb_Encode(msg, minitable, options, arena, &buf, &size)
│ wire/encode.c
▼
encode_message ← 从最后一个字段往第一个倒着遍历
│ 对每个字段:先写值,需要长度前缀时往左"前置" varint
├─► 子消息:先写完 payload,再前置 length,再前置 tag
├─► packed 数组:倒着写完所有元素,再前置整段 length + tag
└─► 定长 packed 数组(小端):整段一次 memcpy
│
缓冲区不够时向 arena "借"空间或翻倍增长
倒写让长度前缀无需两遍扫描或回填——序列化严格一遍完成。详见 第 8 章。
6. 本章小结
| 要点 | 出处 |
|---|---|
| arena 的分配不线程安全、生命周期操作线程安全 | mem/arena.h:13-18 |
| “Must be last” 包含 port 层的约定 | mem/arena.h:30-31 |
upb_MiniTable 紧凑结构 | mini_table/internal/message.h:71-94 |
upb_MiniTableField 12 字节结构 | mini_table/internal/field.h:21-35 |
| 字段查找:dense 快路径 + 二分回落 | mini_table/internal/message.h:283-309 |
| aarch64 调优的二分查找 | mini_table/internal/message.h:181-263 |
生成消息仅 { upb_Message base; } | descriptor.upb.h:21-23 |
| 生成的静态 MiniTable | descriptor.upb_minitable.c:29-39 |
| 可选反射 | README.md:34-36 |
| weak alias + 占位 MiniTable 支撑裁剪 | port/def.inc:695-754 |
upb_DefPool 反射入口 | reflection/def_pool.h:28-40 |
解析入口 upb_Decode | wire/decode.c:1224 |