Wire 解析:尾调用快速解码器

这是 upb 的皇冠明珠。upb 有两个二进制解码器:表驱动的主解码器(wire/decode.c)和可选的 fasttable 尾调用解码器(wire/decode_fast/)。本章拆解 varint 快路径、epsilon-copy 输入流、主解码器分派、fasttable 尾调用链,以及 longjmp 错误处理——这是全系列最详细的一章。

1. varint 快路径:单字节情况全内联

upb 把每次 varint 读取拆成内联的单字节快路径和 out-of-line 的长 varint 路径。热入口 upb_WireReader_ReadVarintwire/internal/reader.h:41-53):

uint8_t byte = *ptr;
if (UPB_LIKELY((byte & 0x80) == 0)) {   // 单字节值 (0..127)
  *val = byte;
  return ptr + 1;
}
... = _upb_WireReader_ReadLongVarint(ptr, byte, stream);

常见情况(值 < 128)就是一次 load、一次分支、一次指针前移——无循环、无边界检查(边界由 IsDone() 摊销,见 §2)。这里的 ConsumeBytes(10) 在 release 下是 no-op(eps_copy_input_stream.h:144-152#ifndef NDEBUG 守卫),只在 debug 下扣减一个”保证可读字节”预算以抓越界 bug。

有三个近乎相同的特化入口,让续行知道最大宽度和上界:ReadVarint(完整 64 位)、ReadTag(封顶 UINT32_MAX:55)、ReadSize(封顶 INT32_MAX:69)。

长路径 _upb_WireReader_ReadLongVarintwire/reader.c:19-31)标 UPB_NOINLINE,循环到第 9 字节:

for (int i = 1; i < 10; i++) {
  uint64_t byte = (uint8_t)ptr[i];
  val += (byte - 1) << (i * 7);     // (byte-1) 抵消续行位
  if (!(byte & 0x80)) return {ptr + i + 1, val};
}

(byte-1)<<(i*7) 的技巧:调用方已把含 0x80 位的首字节整个加进 val,每个后续字节减 1 既抵消续行位、又把 7 个有效位移到位。tag/size 变体只循环到第 5 字节并拒绝越界的 32 位值(reader.c:33-46:48-61)。

为什么快:占多数的小值情况全内联成几条指令、零函数开销、零边界检查;罕见的多字节情况被隔离在冷的非内联函数里。

2. epsilon-copy 输入流:把”每字节检查”摊销成”每字段检查”

这是让上面所有读取都能跳过逐字节边界检查的结构性优化。定义在 wire/internal/eps_copy_input_stream.h

不变量

kUpb_EpsCopyInputStream_SlopBytes = 16:33)。契约是:只要 IsDone() 返回 false,解码器就可无任何边界检查地往后读最多 16 字节。 一个 tag 最多 5 字节、一个最大标量 varint 最多 10 字节(合 15),16 覆盖任何单字段的 tag+值;取 16 而非 15 是为了让定长拷贝对齐好看(注释在 :25-33)。

流结构(:39-54)跟踪:end(可越读 16 字节)、limit_ptr(真正的边界阈值)、limitinput_delta(patch 缓冲与真实指针的偏移,用于溯源),以及 patch[32]双倍 SlopBytes 的 scratch 缓冲,:53)。

边界检查如何被摊销

IsDone 被拆成一个全内联的状态检查 IsDoneStatus:170-183):常见的 “NotDone” 情况就是 ptr < limit_ptr 一次比较。因为 IsDone 保证了 16 字节可读,随后的字段读取根本不做边界检查——这就是全部收益。

patch 缓冲:无逐字节检查地处理缓冲边界

slop 有两种实现:

  1. 大缓冲Init:64-84)令 end = ptr + size - 16,真实输入的最后 16 字节充当天然 slop。
  2. 尾部 / 小缓冲:当解析指针越过 endIsDoneStatus 返回 NeedFallback,触发 IsDoneFallbackeps_copy_input_stream.c:25-47):把真实数据的末 16 字节 memcpy 进 patch[0..15]patch[16..31] 清零、把 end/ptr 重指进 patch 缓冲、记录 input_delta。于是越过真实数据最多 16 字节的读取落进保证为零的 patch 上半区,而非越界内存。

GetInputPtr:220-229)经 input_delta 从 patch 指针还原出原始输入地址——这是被捕获的未知字段/字符串区域仍能锚定真实缓冲的原因。PushLimit/PopLimit:292-317)纯靠调整 limit/limit_ptr 实现子消息长度限制。

为什么快:每字段一次比较,而非每字节一次;缓冲边界处理被推进只拷固定 16 字节的冷回落里。

3. 表驱动主解码器

入口与顶层循环

upb_Decodedecode.c:1224)在栈上建 upb_ErrorHandlerupb_Decoderupb_Decoder_Decode:1201)用 setjmp 包住解析(见 §5)。_upb_Decoder_DecodeMessage:1169)是每消息循环:do { ptr = DecodeField(...); } while (!message_is_done);。零字段且不可扩展的消息短路到 DecodeEmptyMessage:1122),把所有值当一整块未知 blob 跳过。

tag → 字段 → wire 类型分派

_upb_Decoder_DecodeField:1107)先尝试 fasttable(§4);未命中/禁用则 IsDone 后走 DecodeFieldNoFast:1042):

  1. DecodeFieldTag:999)读 tag、拆 field_number = tag>>3wire_type = tag&7
  2. EndGroup 在此识别(:1052)。
  3. DecodeFieldData:1011)做字段查找 + wire 值解码。

字段查找 FindField:745)调 upb_MiniTable_FindFieldByNumber;未命中查扩展注册表(:723)或返回哨兵。wire 值解码 DecodeWireValue:873)是对 wire_typeswitch:Varint(:888,含闭枚举校验和 Munge zigzag/bool 归一化,:139)、32/64Bit(:902:908)、Delimited(:914,经 GetDelimitedOp 选 op——packed op 在此选定,:810)、StartGroup(:918)。

存已知字段、子消息递归、packed、字符串

DecodeKnownField:936)按字段模式分派(:955):Array / Map / Scalar。

  • 子消息递归 + 深度限制DecodeSubMessage:207)压限制,RecurseSubMessage:191)做 if (--d->depth < 0) throw,递归回 DecodeMessage,再 depth++
  • packed 重复:定长 packed 把整段读成临时字符串再批量 memcpy(大端逐元素字节交换,:243-287);varint packed 压限制循环读(:289);枚举 packed 额外校验每个值(:314)。
  • 字符串——别名 vs 拷贝:核心 _upb_Decoder_ReadStringwire/internal/decoder.h:244-263)总是先别名进原始输入缓冲;若未设 kUpb_DecodeOption_AliasString 则 arena malloc + memcpy 一份私有拷贝(:256-259),设了则 StringView 继续指向输入——零拷贝。string 字段的 UTF-8 校验也在此(:251)。

未知字段 DecodeUnknownField:967)用 upb_EpsCopyCapture 记录原始字节,再以别名/拷贝/合并模式追加到消息(模式由 GetAddUnknownMode 选,:118)。

4. fasttable 解码器:musttail 尾调用链

这是皇冠明珠:一串经 musttail 调用的特化解析函数,每个对应一个 (类型, 基数, tag 长度) 组合,用 preserve_none 调用约定把全部状态留在寄存器里。

寄存器传参的调用约定

每个 fast 解析器签名都是 UPB_PARSE_PARAMSdispatch.h:32-35):

upb_Decoder *d, const char *ptr, upb_Message *msg, const upb_MiniTable *table,
uint64_t hasbits, uint64_t data, uint64_t data2

声明为 UPB_PRESERVE_NONE(= __attribute__((preserve_none))port/def.inc:446)+ 用 UPB_MUSTTAILport/def.inc:434)。preserve_none 意味着被调者可踩所有寄存器、没有 callee-saved 寄存器需要跨调用保存/恢复;配合 musttail,每次解析器→解析器的转移编译成一条普通 jmp,7 个参数全程活在寄存器里。

16 位 tag 索引 fasttable

分派循环 upb_DecodeFast_Dispatchdispatch.h:69-86):IsDoneStatus 检查 → _upb_FastDecoder_LoadTag单次 2 字节 memcpy 载 tag(:39-43)→ 打包进 data2 尾调用 TagDispatchTagDispatch:45-64):

uint8_t mask = upb_DecodeFastData2_GetMask(data2);   // mask 在 data2 高位
size_t ofs = data2 & mask;                            // 低 tag 位掩码
const _upb_FastTable_Entry* ent = &table->fasttable[ofs >> 3];
UPB_MUSTTAIL return ent->field_parser(d, ptr, msg, table, hasbits,
                                      ent->field_data, data2);

于是 tag 的低位索引一张最多 32 项的 fasttable。每个 _upb_FastTable_Entry{uint64_t field_data; _upb_FieldParser* field_parser;}mini_table/internal/message.h:31-34),作为柔性数组挂在 MiniTable 上(:92)。表由 upb_DecodeFast_BuildTable 离线构建(select.c:227),字段号越低视为越热。

data / data2 打包

datafield_data)打包字段静态布局(data.h:16-44):offset(16) | case_offset(16) | presence(8) | subofs(8) | expected_tag(16)data2 是运行时分派状态(data.h:71-107):mask(8) | ... | actual_tag(16)

一个解析器做什么

字段解析器经宏在全交叉积上生成。例如 field_varint.c:310-327 实例化 {Scalar,Oneof,Repeated,Packed} × {Bool,Varint32/64,ZigZag32/64,ClosedEnum} × {Tag1Byte,Tag2Byte}。非 packed 标量核心 upb_DecodeFast_Unpackedcardinality.h:393-437):

  1. CheckTag:314-341)比对 expected_tag(来自 data)与 actual_tag(来自 data2);不匹配则翻转 packed↔unpacked 或退出到 FallbackMismatchedSlot;匹配则按 tag 长度推进 ptr
  2. GetScalarField:243)置 hasbit(在寄存器里hasbits!)或 oneof case 并算出目的地。
  3. 类型特化的 single 回调(如 SingleVarintfield_varint.c:62)读/归一化值并存。
  4. 重复字段循环:TryMatchTag + NextRepeatedcardinality.h:274:290不重新分派地连续消费同字段的多个元素,增长进预分配数组。

关键:hasbits 一路在寄存器里携带,只在消息结束或重复字段前才刷回内存(SetHasbitsdispatch.h:88-92)——连续标量字段从不触碰内存里的 hasbit 字。

每个生成函数末尾经 UPB_DECODEFAST_NEXTMAYBEPACKED 宏(cardinality.h:116-152)尾分派——一个对 upb_DecodeFastNext 枚举(:39-83)的 switch,把小枚举结果转成对应的 UPB_MUSTTAIL(回 Dispatch、去回落、去 packed/unpacked 孪生函数等)。这种”返回枚举再 musttail”是绕开 musttail “不能从任意点条件尾调用”限制的有意手法(注释 :35-37:183-189)。

子消息 field_message.c 递归回 DecodeMessagefield_message.c:39)——这种跨调用约定的递归正是主解码器若干函数被标 UPB_PRESERVE_NONE 的原因(decode.c:1165-1168)。

“done” 与回落路径

  • Done / 刷新缓冲MessageIsDoneFallbackdispatch.c:20-51):真 Done 则置 message_is_done、刷 hasbits、检查必填字段;NeedFallback 则刷新缓冲再尾调回分派。
  • 回落到 MiniTable_upb_FastDecoder_DecodeGenericfield_generic.c:25-33)刷 hasbits、原样返回 ptr;控制回到 TryDecodeMessageFastdecode.c:1063),见 message_is_done==false 即在 *ptr 处续慢路径。
  • 槽冲突 / tag 不匹配DecodeMismatchedSlotfield_mismatch.c:29)判断是未知字段、扩展还是长 tag,可能完全在 fast 解码器内处理未知字段(field_unknown.c:119-225)。

fast 解码器由 TryDecodeMessageFastdecode.c:1063)进入,门控于 table_mask != 0xff 且未禁用(:1070)。AlwaysValidateUtf8 会强制禁用 fasttable(wire/internal/decoder.h:96-99)。

为什么快:分派是一次掩码索引 + 间接 jmp;每字段状态跨 preserve_none+musttail 转移全留寄存器(无栈流量、无溢出);每字段常见成本就是一次 tag 比较、一次值读、一次存,hasbit 在寄存器里累积。

5. longjmp 错误处理与 arena 分配

upb_ErrorHandler 就是 {int code; jmp_buf buf;}base/error_handler.h:62-65)。upb_Decoder_Decodedecode.c:1201-1213)建落点:if (UPB_SETJMP(err->buf) == 0) { code = DecodeTop(...); }。任何错误(OOM、格式错误、UTF-8、超深)调 ThrowErrorerror_handler.h:71-76)设 code 并 longjmp 一跳回来,一次性展开整棵递归解析,热路径上没有逐层 if(err) return 检查。fast 解码器的错误辅助不能是 noreturn(会挡住 musttail),所以用单独编译的 _upb_FastDecoder_ErrorJmp2dispatch.c:53-56)。

解码器用单个 bump 指针 upb_Arena 分配一切。为了局部性,它把调用方 arena 按值换进自己结构里的一个本地副本:Init_upb_Arena_SwapInDestroy_upb_Arena_SwapOutwire/internal/decoder.h:114-127),注释解释这”为性能违反 arena 封装”——临时表内 arena 只需支持分配,于是它的热字段与解码器其他热字段相邻。每个分配点检查 NULL 并经同一 longjmp 抛 OOM。

6. 本章小结

机制 出处
单字节 varint 快路径 wire/internal/reader.h:41-53
长 varint (byte-1)<<7i 技巧 wire/reader.c:19-31
SlopBytes=16 与理由 eps_copy_input_stream.h:25-33
流结构(end/limit_ptr/patch[32]) eps_copy_input_stream.h:39-54
IsDoneStatus 一次比较 eps_copy_input_stream.h:170-183
IsDoneFallback patch 拷贝 + 清零 eps_copy_input_stream.c:25-47
upb_Decode 入口 + setjmp wire/decode.c:1201-1224
每消息循环 wire/decode.c:1169
tag 拆分 / wire 值 switch wire/decode.c:873-934
子消息递归 + 深度限制 wire/decode.c:191-218
字符串别名 vs 拷贝 wire/internal/decoder.h:244-263
UPB_PARSE_PARAMS 寄存器约定 decode_fast/dispatch.h:32-37
TagDispatch:掩码→槽→musttail decode_fast/dispatch.h:45-64
data/data2 打包 decode_fast/data.h:16-107
Unpacked 标量核心 decode_fast/cardinality.h:393-437
hasbits 寄存器累积、延迟刷回 decode_fast/dispatch.h:88-92
NEXTMAYBEPACKED musttail switch decode_fast/cardinality.h:116-152
子消息递归回主解码器 decode_fast/field_message.c:39
回落到 MiniTable decode_fast/field_generic.c:25-33
fast 路径门控 wire/decode.c:1063-1070
upb_ErrorHandler + longjmp base/error_handler.h:62-76
arena swap-in/out 局部性 wire/internal/decoder.h:114-127

本站访问量

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