MiniTable 表驱动元数据
upb 在热路径上完全不做运行时反射。每个消息类型由一张
upb_MiniTable描述:一个极小、只读、对 cache 友好的结构,加上一个扁平的 12 字节字段数组。解析、序列化、字段访问全部直接索引这些数组。同一张表既能由 protoc 插件发射成静态 C 初始化器,也能在运行时从紧凑的 MiniDescriptor 字符串重建。本章拆解表的结构、位压缩和编码。
1. upb_MiniTable:紧凑且快查
定义在 mini_table/internal/message.h:71-94:
fields——指向扁平字段数组(:72)size——消息字节大小,须 8 字节对齐(kUpb_Message_Align,:63)(:76)field_count(:78)ext——upb_ExtMode压进一个uint8_t(:80)dense_below(:81)、table_mask(:82)、required_count——必填字段占最低的若干 hasbit(:83)fasttable[]——柔性数组,仅在UPB_FASTTABLE时存在(:92)
值得注意的是 subs(子消息 / 枚举指针)不是结构体成员。它们放在字段数组紧后面的一个独立 upb_MiniTableSubInternal 数组里,每个字段存一个到对应 sub 项的相对偏移(submsg_ofs,以 4 字节为单位)——见 upb_MiniTable_GetSubMessageTable(message.h:139-146),kUpb_SubmsgOffsetBytes == 4(field.h:38)。这样既省掉表里一个独立指针,又让 subs 与字段数据相邻、对 cache 友好。
upb_ExtMode(message.h:41-55):NonExtendable=0、Extendable=1、IsMessageSet=2,外加建表期偷用的两位 IsMapEntry=4 和 AllFastFieldsAssigned=8;_upb_ExtMode_Base 用 7 掩掉 fasttable 状态位(:58-60)。
1.1 dense_below 与 table_mask——两个独立的快查机制
这是两件不相干的事,容易混淆:
dense_below 服务”按字段号找字段”。因为 protoc 插件(和 MiniDescriptor 解码器)按字段号升序发射字段,凡是字段号正好是 1,2,3,… 的前缀都能直接数组索引。dense_below 记录这段稠密前缀的长度。upb_MiniTable_FindFieldByNumber(message.h:283-309)算 i = number - 1,若 i < dense_below 立即返回 &fields[i]——一次比较、一次索引取数、不查找(:289-292)。它在解码 MiniDescriptor 首次遇到”跳号”时被定格(mini_descriptor/decode.c:511-514),或对完全稠密的消息设为字段总数(:526-528)。
table_mask 与 FindFieldByNumber 毫无关系,它驱动 fasttable wire 解析分派(详见 第 7 章)。0xFF(-1)表示”无 fasttable / 已禁用”(wire/decode.c:1070)。否则它是 fasttable 数组上的一个 2 的幂减一掩码:快速解码器读 2 字节 tag,ofs = data2 & mask; ent = &table->fasttable[ofs >> 3](decode_fast/dispatch.h:49-54),于是一个 wire tag 直接变成一个特化解析函数,无需对字段类型做分支。掩码由 upb_DecodeFast_GetTableMask(table_size) 算出(decode_fast/select.h:49-51)。
required_count 喂给 _upb_MiniTable_RequiredMask(message.h:427-432),构造一个”最低 N 个 hasbit 全 1”的掩码,让解析器用一次掩码比较就验证所有必填字段是否齐备。
1.2 稀疏字段的二分回落
当 i >= dense_below,FindFieldByNumber 先早退(number 超过最后一个字段号则返回 NULL,:299-302),再在 [dense_below, field_count) 上经 upb_MiniTable_LowerBound 二分(:265-281)。二分有两套等价实现:通用版(:161-176)和手工调优的 aarch64 版(upb_MiniTable_ArmOptimizedLowerBound,:181-263),后者利用 sizeof(upb_MiniTableField)==12(乘 3 再乘 4 的步长)做地址生成、并在小核上双发射;UPB_STATIC_ASSERT 钉死了字段 12 字节、number 在偏移 0(:187-188、:208-210)。debug 下两套都跑并断言相等(:268-273)。
2. upb_MiniTableField:12 字节里塞下一切
定义在 field.h:21-35,正好 12 字节:
| 成员 | 类型 | 含义 |
|---|---|---|
number | uint32_t(偏移 0) | 字段号(:22) |
offset | uint16_t | 数据在消息内的字节偏移(:23) |
presence | int16_t | >0 = hasbit 索引;<0 = ~oneof_case_offset;0 = 无 presence(:24) |
submsg_ofs | uint16_t | 到 sub 项的相对偏移(uint32 单位),非消息/枚举为 kUpb_NoSub=0xFFFF(:29、:37) |
descriptortype | uint8_t | proto FieldDescriptorProto.Type(:31) |
mode | uint8_t | 模式/标志/表示打包字节(:34) |
mode 这一个字节打包三样东西(:33):
- bit 0-1:
upb_FieldMode——Map=0、Array=1、Scalar=2,掩码kUpb_FieldMode_Mask=3(:40-47)。 - bit 2-5:
upb_LabelFlags——IsPacked=4、IsExtension=8、IsAlternate=16(:50-59)。IsAlternate是个省空间妙招:开放(proto3)枚举按Int32存、未做 UTF-8 校验的字符串按Bytes存,用这个标志记住”其实是 Enum / String”,由upb_MiniTableField_Type还原(:119-128)。 - bit 6-7:
upb_FieldRep左移kUpb_FieldRep_Shift=6(:62-73)。表示有1Byte=0、4Byte=1、StringView=2、8Byte=3;NativePointer按平台经UPB_SIZE别名 4 或 8 字节(:68-69)。
布局分配器真正需要的是 表示(rep,存储类)而非完整类型——把 18 种 proto 类型坍缩成 4 种 rep,才能廉价地按对齐把字段分组。
presence 编码(field.h:135-193):hasbit 的掩码是 1 << (index%8)(:140-145)、字节在 index/8(:147-152);oneof 字段的 case 偏移是 ~presence(:189-193);IsInOneof 就是 presence < 0(:159-162)。
UPB_SIZE的双偏移:UPB_SIZE(size32, size64)在 32 位平台选前者、否则后者(port/def.inc:107-111)。生成代码用它,使同一份源码在两种 ABI 都给出正确偏移。运行时平台固定,所以一个upb_MiniTableField.offset是单一具体的uint16_t;双值只活在静态初始化器和 size 表里(如 size_log2.h:25-70)。
扩展项 upb_MiniTableExtension(extension.h:21-27)把一个 upb_MiniTableField 作为首成员内嵌(于是可被当作字段指针别名),再加一个内联的 upb_MiniTableSub 联合和 extendee 指针;sub 联合就是两个重叠指针(sub.h:14-19)。
枚举表 upb_MiniTableEnum(enum.h:16-20)存 mask_limit、value_count 和一段柔性 data[](先是位掩码字、后是溢出值);CheckValue 先测 64 位快掩码、再测到 mask_limit 的更宽掩码、最后线性扫溢出值(enum.h:26-49)。
3. MiniDescriptor:紧凑编码与运行时重建
MiniDescriptor 是 upb 兼顾”小”与”灵活”的关键:动态语言不必携带完整的 FileDescriptorProto 反射数据,只需一个短字符串,就能在运行时重建出和编译期内建一样快的布局。
字母表与 token 在 mini_descriptor/internal/wire_constants.h:16-68:编码类型 kUpb_EncodedType_*(重复字段在类型上加 RepeatedBase=20)、字段修饰符 kUpb_EncodedFieldModifier_*(FlipPacked / IsRequired / IsProto3Singular / FlipValidateUtf8)、各字符区间、版本标签(Enum '!'、Extension '#'、Message '$'、Map '%'、MessageSet '&')。
base92:字节经 _kUpb_ToBase92 / _kUpb_FromBase92 查表映射(base92.c:10-26),覆盖可打印 ASCII ' '..'~' 去掉 " 和 \ 共 92 个符号。varint 是小端 base-92,位宽按取值范围动态算:_upb_Base92_DecodeVarint 用 log2Ceiling(...) 求 bits_per_char 累加直到字符落在范围外(base92.h:34-55)。
解码器(mini_descriptor/decode.c):upb_MtDecoder(:83-99)持有一个栈上的 upb_MiniTable table、增长中的 fields 数组、oneofs 向量,以及一个巧妙的 rep_counts_offsets[](先统计每种 rep 的字段数,随后被复用来存每种 rep 的累计基偏移)。顶层 upb_MtDecoder_DoBuildMiniTableWithBuf(:765-829)先把表初始化为 table_mask=-1、dense_below=0(:768-773),读 1 字符版本标签(:781-784)后路由到 ParseMap / ParseMessage / ParseMessageSet(:784-803)。
主字段循环 upb_MtDecoder_Parse(:465-531)逐字符走:字段字符→分配下一字段、自增字段号、经 upb_MiniTable_SetField(:176-226)解码类型;修饰符字符→ParseModifier(:415-434);End 字符→进入 oneof 解析;跳号字符→解 base92 varint 推进字段号、并在此定格 dense_below(:509-520)。
4. 布局计算:偏移与 hasbit 的分配
对一条 MessageV1,按序运行(decode.c:789-794):
AssignHasbits(:615-648):hasbit 从kUpb_Reserved_Hasbits = sizeof(upb_Message)*8起(:52-56),把消息头占掉的低位让出来。必填字段先分配、占最低的 hasbit(:619-628),于是required_count映射到低位掩码;随后是可选 hasbit 字段(:638-644)。CalculateAlignments(:573-609):先把非 oneof 字段按 rep 计数(:574-580),再从最小对齐(1Byte)往大走,对齐base并把每个条目从”计数”转成该 rep 组的”基偏移”(:592-602)。从小到大排,能把低对齐字段塞进 hasbyte 前缀留下的空隙、不浪费填充(理由在 :586-591)。AssignOffsets(:657-689):每个非 oneof 字段经Place(:650-655)领走rep_counts_offsets[rep]并按 rep 大小递增——把每种 rep 的基偏移变成一串具体偏移。oneof 成员共享一个 case 偏移槽(固定 4 字节)加一个按最大成员定大小的数据槽。最后size向上取整到kUpb_Message_Align(:687-688)。
Map 绕过布局:ParseMap(:722-747)校验恰好 2 个字段(key=1、val=2),偏移硬编码到 offsetof(upb_MapEntry, k/v)、size = sizeof(upb_MapEntry),并置 IsMapEntry。MessageSet(:749-763)全部固定。
若开启 UPB_FASTTABLE,布局后还会在栈上经 upb_DecodeFast_BuildTable(≤32 项)建分派表,再分配带 fasttable 柔性数组的真表、拷入 field_parsers 并存好 table_mask(:810-826)。
编码器侧在 mini_descriptor/internal/encode.c:PutField(:236-248)发可选跳号、类型字符、修饰符;只有当字段与消息默认不同才发修饰符,以保持串短。
5. 本章小结
| 机制 | 出处 |
|---|---|
upb_MiniTable 结构 | mini_table/internal/message.h:71-94 |
| subs 经相对偏移、与字段相邻 | message.h:139-146、field.h:38 |
FindFieldByNumber dense + 二分 | message.h:283-309 |
| aarch64 调优二分 | message.h:181-263 |
RequiredMask | message.h:427-432 |
upb_MiniTableField 12 字节 | field.h:21-35 |
mode 打包 FieldMode/LabelFlags/Rep | field.h:33-73 |
IsAlternate 省空间 | field.h:119-128 |
| presence 编码 | field.h:135-193 |
UPB_SIZE 双偏移 | port/def.inc:107-111 |
upb_MiniTableEnum CheckValue | enum.h:16-49 |
| MiniDescriptor 字母表/版本标签 | wire_constants.h:16-68 |
| base92 表与 varint | base92.c:10-26、base92.h:34-55 |
| 解码器主循环、dense_below 定格 | decode.c:465-531、:509-528 |
| AssignHasbits / CalculateAlignments / AssignOffsets | decode.c:615-689 |
| 顶层构建 + fasttable 建表 | decode.c:765-829 |