Proto 序列化不是规范化的
解释序列化的工作原理以及为什么它不是规范化的。
许多人希望序列化后的 proto 能规范地代表其内容。常见用例包括:
- 将序列化的 proto 用作哈希表的键
- 对序列化的 proto 取指纹或校验和
- 通过比较序列化后的数据判断消息是否相等
不幸的是,protobuf 的序列化不是(也无法做到)规范化。虽然有一些特殊情况(如 MapReduce),但通常你应该认为 proto 的序列化结果是不稳定的。本文将解释原因。
确定性并不等于规范化
确定性序列化并不等于规范化。序列化器可能因多种原因生成不同的输出,包括但不限于以下变化:
- protobuf 的 schema 发生任何变化。
- 构建的应用发生任何变化。
- 二进制文件使用不同的编译参数(如 opt 与 debug)。
- protobuf 库被更新。
这意味着序列化 proto 的哈希值是脆弱的,无法在不同时间或空间保持稳定。
导致序列化输出变化的原因有很多,上述列表并不详尽。其中有些是该问题领域内固有的困难,即使我们想要,也难以保证规范化序列化。还有一些是我们有意未定义的,以便为优化留出空间。
稳定序列化的固有障碍
Protobuf 对象会保留未知字段,以实现前向和后向兼容性。未知字段无法规范化序列化:
- 未知字段无法区分字节和子消息,因为它们的 wire type 相同。这导致无法对存储在未知字段集中的消息进行规范化。如果要规范化,我们需要递归进入未知子消息并按字段号排序,但我们没有足够的信息来做到这一点。
- 未知字段总是在已知字段之后序列化,以提高效率。但规范化序列化要求按字段号将未知字段与已知字段交错序列化。这会给所有人带来效率和代码体积的开销,即使他们并未使用该特性。
有意未定义的内容
即使规范化序列化是可行的(即我们能解决未知字段问题),我们也会有意将序列化顺序未定义,以便获得更多优化空间:
- 如果我们能证明某个字段在二进制中从未被使用,可以将其从 schema 中完全移除,并作为未知字段处理。这能显著节省代码体积和 CPU 周期。
- 有时可以通过将同一字段的向量一起序列化来优化,尽管这会打破字段号顺序。
为了给这些优化留出空间,我们希望在某些配置下有意打乱字段顺序,防止应用错误地依赖字段顺序。