Proto 序列化不是规范化的

解释序列化的工作原理以及为什么它不是规范化的。

许多人希望序列化后的 proto 能规范地代表其内容。常见用例包括:

  • 将序列化的 proto 用作哈希表的键
  • 对序列化的 proto 取指纹或校验和
  • 通过比较序列化后的数据判断消息是否相等

不幸的是,protobuf 的序列化不是(也无法做到)规范化。虽然有一些特殊情况(如 MapReduce),但通常你应该认为 proto 的序列化结果是不稳定的。本文将解释原因。

确定性并不等于规范化

确定性序列化并不等于规范化。序列化器可能因多种原因生成不同的输出,包括但不限于以下变化:

  1. protobuf 的 schema 发生任何变化。
  2. 构建的应用发生任何变化。
  3. 二进制文件使用不同的编译参数(如 opt 与 debug)。
  4. protobuf 库被更新。

这意味着序列化 proto 的哈希值是脆弱的,无法在不同时间或空间保持稳定。

导致序列化输出变化的原因有很多,上述列表并不详尽。其中有些是该问题领域内固有的困难,即使我们想要,也难以保证规范化序列化。还有一些是我们有意未定义的,以便为优化留出空间。

稳定序列化的固有障碍

Protobuf 对象会保留未知字段,以实现前向和后向兼容性。未知字段无法规范化序列化:

  1. 未知字段无法区分字节和子消息,因为它们的 wire type 相同。这导致无法对存储在未知字段集中的消息进行规范化。如果要规范化,我们需要递归进入未知子消息并按字段号排序,但我们没有足够的信息来做到这一点。
  2. 未知字段总是在已知字段之后序列化,以提高效率。但规范化序列化要求按字段号将未知字段与已知字段交错序列化。这会给所有人带来效率和代码体积的开销,即使他们并未使用该特性。

有意未定义的内容

即使规范化序列化是可行的(即我们能解决未知字段问题),我们也会有意将序列化顺序未定义,以便获得更多优化空间:

  1. 如果我们能证明某个字段在二进制中从未被使用,可以将其从 schema 中完全移除,并作为未知字段处理。这能显著节省代码体积和 CPU 周期。
  2. 有时可以通过将同一字段的向量一起序列化来优化,尽管这会打破字段号顺序。

为了给这些优化留出空间,我们希望在某些配置下有意打乱字段顺序,防止应用错误地依赖字段顺序。