Protobuf 存负数为什么这么费——sint32 与 ZigZag 编码

上篇验证过用 int64 存玩家 ID(234231233)只需要 5 个字节,感觉 Protobuf 挺能省的。结果最近处理战斗协议时发现一件让我疑惑的事:把 hp_delta = -1int32 编码,居然要 10 个字节。

JSON 写 {"hp_delta":-1} 才 15 字节,Protobuf 存个 -1 就花了 10 个字节,比 JSON 没省多少,这是怎么回事?

负数在 VARINT 里是个大数

VARINT 的编码思路是数越小字节越少,每个字节用 7 位存数据,最高位标记后面是否还有字节。所以 1 只需 1 字节,300 只需 2 字节。

但负数在计算机里是补码,-1 的 32 位补码是 0xFFFFFFFF,本质上是个很大的正整数。更麻烦的是,Protobuf 对 int32 的负值编码前会先符号扩展成 64 位:

scss 复制代码
int32(-1)  → 0xFFFFFFFF
int64(-1)  → 0xFFFFFFFFFFFFFFFF
VARINT     → FF FF FF FF FF FF FF FF FF 01

10 个字节,而且不管你的负数是 -1 还是 -9999,只要是 int32 负值,一律 10 字节,没有例外。

ZigZag 的思路

官方解法是 sint32 / sint64,背后是 ZigZag 编码。思路是把有符号整数重新映射成无符号整数,让绝对值小的数映射结果也小:

原始值 ZigZag 结果
0 0
-1 1
1 2
-2 3
2 4
-128 255
128 256

就是把整数轴从中间折叠,正负交错排列。公式是:

scss 复制代码
ZigZag(n) = (n << 1) ^ (n >> 31)

>> 这里是算术右移,负数右移后全是 1。拿 -1 算一下:

ini 复制代码
n = -1,二进制:1111 1111 1111 1111 1111 1111 1111 1111

n << 1  = 0xFFFFFFFE
n >> 31 = 0xFFFFFFFF

XOR:0xFFFFFFFE ^ 0xFFFFFFFF = 1

-1 变成了 1,VARINT 只要 1 字节。再算个 -300

ini 复制代码
n = -300  →  0xFFFFFED4

n << 1  = 0xFFFFFDA8
n >> 31 = 0xFFFFFFFF

XOR:0xFFFFFDA8 ^ 0xFFFFFFFF = 0x257 = 599

599 的 VARINT 按 7 位分组:

scss 复制代码
低 7 位:101 0111 → 有后续 → 0xD7
高 7 位:000 0100 → 最后   → 0x04

sint32(-300) = D7 04    2 字节
int32(-300)  = D4 FD FF FF FF FF FF FF FF 01    10 字节

差了 5 倍。解码反过来:(ZigZag >>> 1) ^ -(ZigZag & 1)>>> 是无符号右移。

在游戏协议里的体感

把上篇的 BattleEvent 改成 sint32 之后,字节数变化很明显。拿三个常见的负数字段举例(hp_delta=-50, dx=-3, score=-100):

字段 ZigZag 结果 sint32 字节 int32 字节
hp_delta = -50 99 1 10
dx = -3 5 1 10
score = -100 199 2 10

加上每个字段 1 字节的 tag,int32 版本是 33 字节,sint32 版本是 9 字节,压到原来 27%。帧同步每秒推 20 帧,这个差距乘上在线人数就比较可观了。

顺便说一下选型,三种整数类型差异挺大,选错了挺亏的:

类型 编码 用在哪
int32 VARINT,负数固定 10 字节 始终非负:技能 ID、物品数量、等级
sint32 ZigZag + VARINT 会出现负数:血量变化、位移增量、坐标差值
fixed32 固定 4 字节 值普遍超过 2^28:时间戳、大范围随机种子

有个坑值得单独说:坐标用 int32 存绝对值没问题,始终是正数。但有人为了省带宽改成存增量,增量有正有负,这时候必须同步把类型改成 sint32,否则玩家往左走反而更费带宽,相当于优化了个负数。

感兴趣的话可以去 tools.ioirb.cn/proto/parse... 把上面计算出来的十六进制丢进去反向解析,把 sint32 改回 int32 对比一下字节数,差距很直观。

用到了AI排版,好看多了。

相关推荐
yinchnag1 天前
proto协议统计带宽字节流量
protobuf
十五年专注C++开发14 天前
C++ 序列化 Protocol Buffers:高效数据交换
开发语言·c++·序列化·反序列化·protobuf
喵了几个咪14 天前
统一范式:中后台Admin项目标准化API分层开发方案(Vue/React通用)
前端·vue.js·react.js·protobuf
明月_清风15 天前
二进制序列化入门——为什么二进制比文本更快、更小?
后端·protobuf·messagepack
love530love1 个月前
ComfyUI MediaPipe 猴子补丁终极完善版:补全上下文管理与姿态检测兼容
人工智能·windows·python·comfyui·protobuf·mediapipe
Maguyusi1 个月前
Ubuntu26.04 编译 abseil-cpp protobuf v33.6
linux·protobuf·abseil
猫吻鱼1 个月前
【笔记03】【Grpc 和 Protobuf】
grpc·protobuf
xiaodaoluanzha1 个月前
golang中MetaMessage(mm)的使用
json·protobuf
小堃学编程2 个月前
【项目实战】基于protobuf的发布订阅式消息队列(4)—— 服务端
c语言·c++·vscode·消息队列·gtest·protobuf·muduo