Сериализация Proto не является канонической

Объясняет, как работает сериализация и почему она не является канонической.

Многие хотят, чтобы сериализованный proto канонически представлял содержимое этого proto. Варианты использования включают:

  • использование сериализованного proto в качестве ключа в хэш-таблице
  • снятие отпечатка (fingerprint) или контрольной суммы сериализованного proto
  • сравнение сериализованных полезных нагрузок как способ проверки равенства сообщений

К сожалению, сериализация protobuf не является (и не может быть) канонической. Существует несколько заметных исключений, таких как MapReduce, но в целом вам следует воспринимать сериализацию proto как нестабильную. Эта страница объясняет, почему.

Детерминированность — это не каноничность

Детерминированная сериализация не является канонической. Сериализатор может генерировать разный вывод по многим причинам, включая, но не ограничиваясь, следующими вариациями:

  1. Схема protobuf изменяется любым способом.
  2. Приложение, которое собирается, изменяется любым способом.
  3. Бинарный файл собирается с разными флагами (например, opt vs. debug).
  4. Библиотека protobuf обновляется.

Это означает, что хэши сериализованных protobuf хрупки и нестабильны во времени или пространстве.

Существует много причин, по которым сериализованный вывод может измениться. Вышеуказанный список не является исчерпывающим. Некоторые из них — это inherent difficulties (внутренние сложности) в проблемном пространстве, которые сделали бы гарантирование канонической сериализации неэффективным или невозможным, даже если бы мы этого захотели. Другие — это вещи, которые мы намеренно оставляем неопределенными, чтобы предоставить возможности для оптимизации.

Внутренние барьеры для стабильной сериализации

Объекты Protobuf сохраняют неизвестные поля для обеспечения прямой и обратной совместимости. Обработка неизвестных полей является основным препятствием для канонической сериализации.

В бинарном формате (wire format) поля bytes и вложенные подсообщения используют один и тот же тип в бинарном формате (wire type). Эта неоднозначность делает невозможным корректную канонизацию сообщений, хранящихся в наборе неизвестных полей. Поскольку одни и те же содержимые могут быть либо тем, либо другим, невозможно узнать, следует ли обрабатывать это как сообщение и рекурсивно спускаться вниз или нет.

Для эффективности реализации обычно сериализуют неизвестные поля после известных полей. Однако каноническая сериализация потребовала бы перемежения неизвестных полей с известными полями в соответствии с номерами полей. Это наложило бы значительные затраты на эффективность и размер кода на всех пользователей, даже на тех, которые не требуют этой функции.

Вещи, намеренно оставленные неопределенными

Даже если бы каноническая сериализация была осуществима (то есть, если бы мы могли решить проблему неизвестных полей), мы намеренно оставляем порядок сериализации неопределенным, чтобы позволить больше возможностей для оптимизации:

  1. Если мы можем доказать, что поле никогда не используется в бинарном файле, мы можем полностью удалить его из схемы и обрабатывать его как неизвестное поле. Это экономит значительный размер кода и такты процессора.
  2. Могут существовать возможности для оптимизации путем сериализации векторов одного и того же поля вместе, даже если это нарушит порядок номеров полей.

Чтобы оставить место для подобных оптимизаций, мы хотим намеренно перемешивать порядок полей в некоторых конфигурациях, чтобы приложения не стали неадекватно зависеть от порядка полей.