整体架构

本章建立 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       │  纯预处理器,无运行时代码
        └──────────────────────────────────────────────┘

各层职责简述:

  • portport/def.incundef.incatomic.hsanitizers.h)——纯预处理器移植/优化层,没有任何运行时代码。每个翻译单元最后都包含它,每个头文件结尾再包含 undef.inc 把宏清干净(见 第 3 章)。所有 always_inline、分支提示、对齐、musttail 检测都集中在这里。
  • memmem/arena.calloc.c)——arena 分配器,整个库的内存模型核心。arena 的分配不是线程安全的(单线程设计,所以分配路径无需原子),但其生命周期操作(fuse/refcount)是线程安全的(mem/arena.h:13-18)。
  • base——status、descriptor 常量、upb_StringView 等所有层共用的基础类型。
  • hash——开放寻址哈希表,被反射和扩展查找复用。
  • lex——JSON 和文本格式用到的数字/unicode 转换。
  • mini_table——运行时”快核心”的数据结构。struct upb_MiniTablemini_table/internal/message.h:71-94)和 struct upb_MiniTableFieldmini_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 与文本格式,依赖 lexreflection
  • reflection——重量级层:upb_DefPoolreflection/def_pool.h:28-40)加上 upb_MessageDefupb_FieldDef 等完整 descriptor 对象。
  • util——基于反射的高层工具。

每个头文件都遵循 “Must be last” 约定:在末尾包含 port 层,例如 mem/arena.h:30-31。这保证 port 层的宏在所有声明之后才生效、且不会泄漏给用户。

2. 核心切分:快核心 vs 反射层

理解 upb 体积优势的关键,是它把 MiniTable 驱动的快核心(mini_table + message + wire)与反射层(DefPool / descriptors)彻底切开。

生成代码只依赖快核心

因为解析/序列化只需要 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_FindFieldByNumbermini_table/internal/message.h:283-309):

  1. dense 快路径:因为字段通常按字段号升序排列,凡是字段号正好是 1,2,3,… 的前缀都能直接用数组下标定位。dense_below 记录这段稠密前缀的长度;若 number-1 < dense_below,一次比较 + 一次索引取出(message.h:288-292),不查找。
  2. 稀疏回落:否则在 [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_Decodesetjmp,热路径上没有逐层的错误返回检查。详见 第 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

本站访问量

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