🌊

【Protobuf】Varintエンコーディングの仕組みとフィールド番号の重要性

2024/05/19に公開

プロトコールバッファーとは

  • プロトコルバッファ(Protocol Buffers、protobuf)は、構造化データのシリアライズ形式で、Googleによって開発された。
  • protobufは、XMLやJSONよりも効率的にデータをシリアライズし、小さいメッセージサイズと高速なパーシングを実現する。
  • 基本的には、アプリとサーバーがProtobufを使ってデータをやり取りする際には、両方が同じメッセージ定義(.protoファイル)を共有している必要がある。これは、メッセージが正しくシリアライズされ、デシリアライズされるために必要。

Protobufの基本

  • .protoファイル: protobufのスキーマは**.proto**ファイルに記述される。このファイルは、メッセージの型を定義し、フィールドの名前、型、番号を指定する。
  • メッセージ: protobufでデータ構造を定義する際に使用する基本的な単位。メッセージは一つ以上のフィールドを持つ。
  • フィールドの型: フィールドには様々な型を指定でき、スカラー型(数値、文字列、ブール値など)、他のメッセージ、列挙型などがある。
  • フィールド番号: 各フィールドにはタグ番号が割り当てられ、この番号はそのフィールドをユニークに識別する。この番号はワイヤーフォーマットで重要な役割を果たす。
syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  bool has_pet = 3;
}

https://protobuf.dev/programming-guides/proto3/

フィールド番号の重要性

フィールド番号は、シリアライズされたデータ内で各フィールドを一意に識別するために使用される。受信側がメッセージをデシリアライズする際、これらの番号を使用して各フィールドのデータを適切に解釈し、元のメッセージ構造を再構築する。

したがって、フィールド番号はメッセージ定義内で一意でなければならず、一度使用された番号は変更することができない(後方互換性を保つため)。

例えば、先ほどのメッセージ定義にに対して**name = "Alice"**, id = 123, **has_pet = true**という値を設定した場合、シリアライズされたメッセージ全体は次のようなバイト列になる。

0a 05 41 6c 69 63 65 10 7b 18 01

メッセージ全体のバイト列において、フィールド番号は各フィールドの値の前にあるキーにエンコードされている。このキーはフィールド番号とそのフィールドの型に基づいて計算された値。

  • 0aname フィールド(フィールド番号1)のキー。
  • 05 :文字列 "Alice" の長さを表すvarint。
  • 41 6c 69 63 65 : "Alice" のUTF-8エンコーディング。
  • 10id フィールド(フィールド番号2)のキー。この場合、ワイヤータイプは0(varint)で、フィールド番号は2。したがってキーは (2 << 3) | 010 になる。
  • 7b :数値 123 を表すvarint
  • 18has_pet フィールド(フィールド番号3)のキー。ワイヤータイプは0で、フィールド番号は3。キーは (3 << 3) | 018 になる。
  • 01 :true(has_pet)を表すvarint

受け取り側はこのバイト列を先頭から順に読み、各キーを解析してどのフィールドがどのような型のデータを持っているかを識別する。そして、適切なデコーディング手法を使用して各フィールドの値を復元する。このプロセスを通じて、元のメッセージ構造が正確に再現される。

ワイヤーフォーマット

ワイヤーフォーマットは、メッセージがバイト列としてシリアライズされる形式。非常に効率的で、メッセージをパースしたりシリアライズしたりする際のオーバーヘッドが非常に小さい。ワイヤーフォーマットのプロセスの中でvarintエンコーディングが使われる。

https://protobuf.dev/programming-guides/encoding/

基本のVarintエンコーディングの仕組み

  • Varintは、整数のサイズに応じて使用するバイト数を変えることができるため、データのサイズを効率的に小さく保つことができる。
  • 各バイトの最上位ビット(MSB、最も左側のビット)は継続ビットと呼ばれ、次のバイトもvarintの一部であるかどうかを示す。このビットが1である場合は、次のバイトもこの数値の一部であることを意味する。
  • 各バイトの残りの7ビットはペイロードとして使われ、これらのビットを結合して最終的な数値を形成する。
message Test1 {
  optional int32 a = 1;
}

Test1 メッセージでフィールド a150 を設定した場合、。a フィールドが1番目のフィールドであると仮定すると、エンコードされたメッセージは 08 96 01 というバイト列になる。

数値シリアライズ後のサイズ比較(VarInt・JSON・XML)

150という数字をVarInt・JSON・XMLでシリアライズした時のデータサイズを比較してみる

  • VarInt:96 01になり2バイト
  • JSON:単純に**150という文字列になる。UTF-8では、標準のASCII文字(0から9の数字を含む)は1バイトでエンコードされるので、150は3バイト必要。キーを含むオブジェクト(例:{"a": 150})では、追加の文字も考慮に入れる必要がある。この例では、{"a":、スペース(省略可)、150}**が含まれるので、合計10バイト(スペースを含まない場合は9バイト)になる。
  • XML:**<a>150</a>となる。<a>は3バイト、150は3バイト、</a>**は4バイトで、合計10バイトが必要。

というわけで、VarInt(2バイト)・JSON(9-10バイト)・XML(10バイト)となり、VarIntを使用するProtoufではデータサイズを小さくできる。

150をvarintエンコーディングしたら9601になる理由

150を二進数に変換すると10010110になる。しかし、、

続きはこちらで記載しています
https://kazulog.fun/dev/protobuf-varint-encoding-and-field-numbers/

Discussion