Protocol Buffer のバイトへのエンコードについて

https://developers.google.com/protocol-buffers/docs/overview を読んでいたところ、Encoding の部分がおもしろかったので、まとめたいと思います。


TL;DR

この記事を読んでわかること


  • どうデータがエンコードされているか

  • タグナンバーは15までに割り振るとなぜ効率が良いのか

この記事を読んでわからないこと


  • なぜ、負数を扱う場合はintではなくsintを選ぶべきなのか(あとで追記するかもしれません)


protobufとは

まず簡単にprotobufについて紹介します。 


src/foo.proto

syntax = "proto3";

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}


https://developers.google.com/protocol-buffers/docs/proto3

このように宣言し、protocコマンドを使い、RubyやC++などの言語ごとにシリアライザ・デシリアライザが生成できます。

Rubyの例: protoc --proto_path=src --ruby_out=build/gen src/foo.protohttps://developers.google.com/protocol-buffers/docs/reference/ruby-generated#invocation

protobufを使うメリットとしては、

1. コードを自動生成してくれるので、スキーマ変更時の手間が少ない

2. 古いバージョンのprotoファイルから生成されたクライアントであっても、タグナンバーの重複さえなければ問題なく動くHow do they work?

3. JSONへのシリアライズ・デシリアライズもできる jsonpb

4. JSONに比べて小さいサイズにシリアライズされる

このうちの4の部分について掘り下げたいと思います。

※注意上ではproto3を使っていましたが、以降はドキュメントに合わせてproto2を使います


バイト列へのシリアライズについて

基本的に、https://developers.google.com/protocol-buffers/docs/encoding についてまとめていきます。

まず、ルールがいくつかあります。

そのルールは大きく 2つに分類できます。

1. データのエンコーディング

2. 型情報・タグナンバーのエンコーディング

で、

message Test1 {

optional int32 a = 1;
}

に対して、a=150とデータを入れると、

08 96 01

このようなバイト列が返ります。 

このとき08の部分が型やタグナンバーの情報が入っていて、96 01の部分に150という情報が入ります。

なぜそのような値になるかについてそれぞれ説明します。 


データのエンコーディング

先程の例の96 01という値をビット列に直すと以下のようになります

1001 0110 0000 0001

まずルールの一つとして、最上位ビット(MSB)は後続ビットの有無を表します。

なので、それを無視すると、

1001 0110 0000 0001

=> 001 0110 0000 0001

この部分はデータとは無関係なものとなります。

で、注意する必要があるのはintをエンコードする際のバイトオーダーはリトルエンディアンで記述されるので、

001 0110 0000 0001

=> 0000 0001 ++ 0010110
=> 10010110
=> 128 + 16 + 4 + 2 => 150

となります。 


型情報・タグナンバーのエンコーディング

型情報と書いていますが、実際にいうと型は5種類に分類されます。

image.png

https://developers.google.com/protocol-buffers/docs/encoding#structure

で、これらを使ってどのように型情報がエンコードされるかというと、先程のa=150の例を見ると、

08 96 01

↑ ここが型情報・タグナンバー

で、08は、ビット列に直すと、

0000 1000

これにはどのように型情報がエンコードされているかというと、ルールが2つあって、

1. データのエンコーディングと同様にMSBで後続ビットがあるかどうかを表す

2. (field_number << 3) | wire_typeで型情報とタグナンバーのエンコードをします

今、aのタグナンバーは1、wire_typeが0なので、

(1 << 3) | 0

=> 1000 | 0
=> 1000
=> 0000 1000

このようになります。

で、なぜタグナンバーが15までにすると良いかというと、MSBがあるので、タグナンバーに使えるのは4ビットまでだからです。

例えば、タグナンバーが15のときは、

(1111 << 3) | 0

=> 1111000 | 0
=> 01111 1000
↑これは後続ビットがあるかどうかを示すのに使われる

となり、15がギリギリ1バイトに収まるからです。