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_GetSubMessageTablemessage.h:139-146),kUpb_SubmsgOffsetBytes == 4field.h:38)。这样既省掉表里一个独立指针,又让 subs 与字段数据相邻、对 cache 友好。

upb_ExtModemessage.h:41-55):NonExtendable=0Extendable=1IsMessageSet=2,外加建表期偷用的两位 IsMapEntry=4AllFastFieldsAssigned=8_upb_ExtMode_Base7 掩掉 fasttable 状态位(:58-60)。

1.1 dense_belowtable_mask——两个独立的快查机制

这是两件不相干的事,容易混淆:

dense_below 服务”按字段号找字段”。因为 protoc 插件(和 MiniDescriptor 解码器)按字段号升序发射字段,凡是字段号正好是 1,2,3,… 的前缀都能直接数组索引。dense_below 记录这段稠密前缀的长度。upb_MiniTable_FindFieldByNumbermessage.h:283-309)算 i = number - 1,若 i < dense_below 立即返回 &fields[i]——一次比较、一次索引取数、不查找(:289-292)。它在解码 MiniDescriptor 首次遇到”跳号”时被定格(mini_descriptor/decode.c:511-514),或对完全稠密的消息设为字段总数(:526-528)。

table_maskFindFieldByNumber 毫无关系,它驱动 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_RequiredMaskmessage.h:427-432),构造一个”最低 N 个 hasbit 全 1”的掩码,让解析器用一次掩码比较就验证所有必填字段是否齐备。

1.2 稀疏字段的二分回落

i >= dense_belowFindFieldByNumber 先早退(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=0Array=1Scalar=2,掩码 kUpb_FieldMode_Mask=3:40-47)。
  • bit 2-5:upb_LabelFlags——IsPacked=4IsExtension=8IsAlternate=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=04Byte=1StringView=28Byte=3NativePointer 按平台经 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_MiniTableExtensionextension.h:21-27)把一个 upb_MiniTableField 作为首成员内嵌(于是可被当作字段指针别名),再加一个内联的 upb_MiniTableSub 联合和 extendee 指针;sub 联合就是两个重叠指针(sub.h:14-19)。

枚举表 upb_MiniTableEnumenum.h:16-20)存 mask_limitvalue_count 和一段柔性 data[](先是位掩码字、后是溢出值);CheckValue 先测 64 位快掩码、再测到 mask_limit 的更宽掩码、最后线性扫溢出值(enum.h:26-49)。

3. MiniDescriptor:紧凑编码与运行时重建

MiniDescriptor 是 upb 兼顾”小”与”灵活”的关键:动态语言不必携带完整的 FileDescriptorProto 反射数据,只需一个短字符串,就能在运行时重建出和编译期内建一样快的布局。

字母表与 tokenmini_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_DecodeVarintlog2Ceiling(...)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=-1dense_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):

  1. AssignHasbits:615-648):hasbit 从 kUpb_Reserved_Hasbits = sizeof(upb_Message)*8 起(:52-56),把消息头占掉的低位让出来。必填字段先分配、占最低的 hasbit(:619-628),于是 required_count 映射到低位掩码;随后是可选 hasbit 字段(:638-644)。
  2. CalculateAlignments:573-609):先把非 oneof 字段按 rep 计数(:574-580),再从最小对齐(1Byte)往大走,对齐 base 并把每个条目从”计数”转成该 rep 组的”基偏移”(:592-602)。从小到大排,能把低对齐字段塞进 hasbyte 前缀留下的空隙、不浪费填充(理由在 :586-591)。
  3. 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.cPutField:236-248)发可选跳号、类型字符、修饰符;只有当字段与消息默认不同才发修饰符,以保持串短。

5. 本章小结

机制 出处
upb_MiniTable 结构 mini_table/internal/message.h:71-94
subs 经相对偏移、与字段相邻 message.h:139-146field.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-26base92.h:34-55
解码器主循环、dense_below 定格 decode.c:465-531:509-528
AssignHasbits / CalculateAlignments / AssignOffsets decode.c:615-689
顶层构建 + fasttable 建表 decode.c:765-829

本站访问量

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