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_ReadVarint(wire/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_ReadLongVarint(wire/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(真正的边界阈值)、limit、input_delta(patch 缓冲与真实指针的偏移,用于溯源),以及 patch[32](双倍 SlopBytes 的 scratch 缓冲,:53)。
边界检查如何被摊销
IsDone 被拆成一个全内联的状态检查 IsDoneStatus(:170-183):常见的 “NotDone” 情况就是 ptr < limit_ptr 一次比较。因为 IsDone 保证了 16 字节可读,随后的字段读取根本不做边界检查——这就是全部收益。
patch 缓冲:无逐字节检查地处理缓冲边界
slop 有两种实现:
- 大缓冲:
Init(:64-84)令end = ptr + size - 16,真实输入的最后 16 字节充当天然 slop。 - 尾部 / 小缓冲:当解析指针越过
end,IsDoneStatus返回NeedFallback,触发IsDoneFallback(eps_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_Decode(decode.c:1224)在栈上建 upb_ErrorHandler 和 upb_Decoder,upb_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):
DecodeFieldTag(:999)读 tag、拆field_number = tag>>3、wire_type = tag&7。- EndGroup 在此识别(:1052)。
DecodeFieldData(:1011)做字段查找 + wire 值解码。
字段查找 FindField(:745)调 upb_MiniTable_FindFieldByNumber;未命中查扩展注册表(:723)或返回哨兵。wire 值解码 DecodeWireValue(:873)是对 wire_type 的 switch: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_ReadString(wire/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_PARAMS(dispatch.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_MUSTTAIL(port/def.inc:434)。preserve_none 意味着被调者可踩所有寄存器、没有 callee-saved 寄存器需要跨调用保存/恢复;配合 musttail,每次解析器→解析器的转移编译成一条普通 jmp,7 个参数全程活在寄存器里。
16 位 tag 索引 fasttable
分派循环 upb_DecodeFast_Dispatch(dispatch.h:69-86):IsDoneStatus 检查 → _upb_FastDecoder_LoadTag 用单次 2 字节 memcpy 载 tag(:39-43)→ 打包进 data2 尾调用 TagDispatch。TagDispatch(: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 打包
data(field_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_Unpacked(cardinality.h:393-437):
CheckTag(:314-341)比对expected_tag(来自data)与actual_tag(来自data2);不匹配则翻转 packed↔unpacked 或退出到FallbackMismatchedSlot;匹配则按 tag 长度推进ptr。GetScalarField(:243)置 hasbit(在寄存器里的hasbits!)或 oneof case 并算出目的地。- 类型特化的
single回调(如SingleVarint,field_varint.c:62)读/归一化值并存。 - 重复字段循环:
TryMatchTag+NextRepeated(cardinality.h:274、:290)不重新分派地连续消费同字段的多个元素,增长进预分配数组。
关键:hasbits 一路在寄存器里携带,只在消息结束或重复字段前才刷回内存(SetHasbits,dispatch.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 递归回主 DecodeMessage(field_message.c:39)——这种跨调用约定的递归正是主解码器若干函数被标 UPB_PRESERVE_NONE 的原因(decode.c:1165-1168)。
“done” 与回落路径
- Done / 刷新缓冲:
MessageIsDoneFallback(dispatch.c:20-51):真 Done 则置message_is_done、刷 hasbits、检查必填字段;NeedFallback则刷新缓冲再尾调回分派。 - 回落到 MiniTable:
_upb_FastDecoder_DecodeGeneric(field_generic.c:25-33)刷 hasbits、原样返回ptr;控制回到TryDecodeMessageFast(decode.c:1063),见message_is_done==false即在*ptr处续慢路径。 - 槽冲突 / tag 不匹配:
DecodeMismatchedSlot(field_mismatch.c:29)判断是未知字段、扩展还是长 tag,可能完全在 fast 解码器内处理未知字段(field_unknown.c:119-225)。
fast 解码器由 TryDecodeMessageFast(decode.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_Decode(decode.c:1201-1213)建落点:if (UPB_SETJMP(err->buf) == 0) { code = DecodeTop(...); }。任何错误(OOM、格式错误、UTF-8、超深)调 ThrowError(error_handler.h:71-76)设 code 并 longjmp 一跳回来,一次性展开整棵递归解析,热路径上没有逐层 if(err) return 检查。fast 解码器的错误辅助不能是 noreturn(会挡住 musttail),所以用单独编译的 _upb_FastDecoder_ErrorJmp2(dispatch.c:53-56)。
解码器用单个 bump 指针 upb_Arena 分配一切。为了局部性,它把调用方 arena 按值换进自己结构里的一个本地副本:Init 调 _upb_Arena_SwapIn、Destroy 调 _upb_Arena_SwapOut(wire/internal/decoder.h:114-127),注释解释这”为性能违反 arena 封装”——临时表内 arena 只需支持分配,于是它的热字段与解码器其他热字段相邻。每个分配点检查 NULL 并经同一 longjmp 抛 OOM。