2016-09-21

Protocol Buffersを触ってみた

Protocol Buffersはデータ構造をシリアライズする技術になります。いわゆるXMLやJSONみたいなものではありますが、バイナリ形式なので「サイズが小さい」「シリアライズ、デシリアライズが高速」でprotoファイルによってメッセージ定義を「シンプルに記述可能」なのが売りです。またメッセージ定義はプログラムにコンパイルされるため、プログラムから利用しやすいのも利点です。

ということで今回はProtocol Buffersを触ってみました。

ダウンロード&インストール

以下のリンクで各言語のパッケージをダウンロードします。今回はPythonを使うので、protobuf-python-3.0.0.zipをダウンロードします。

Release Protocol Buffers v3.0.0 · google/protobuf

$ unzip protobuf-python-3.0.0.zip
$ cd protobuf-3.0.0

まずはコンパイラのprotocコマンドをビルド&インストール

$ ./configure
$ make
$ make check
$ sudo make install
$ sudo ldconfig # refresh shared library cache.

Protocol Buffersを使ってプログラムを記述

Python用のライブラリをインストール

$ cd python # protobuf-3.0.0/python
$ python setup.py build
$ python setup.py test
$ sudo python setup.py install

.protoファイルからソースコードをコンパイル

$ cd {PROJECT_PATH}
$ mkdir src
$ protoc --python_out=src ./addressbook.proto

addressbook.protoはこんな感じで(proto3にしているのはRubyで利用するため)

syntax = "proto3";
package tutorial;

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phone = 4;
}

Pythonスクリプトからは以下のようにして利用します。

#!/usr/bin/env python
import addressbook_pb2
import sys

person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phone.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.HOME

sys.stdout.write(person.SerializeToString())

出力結果はバイナリなのでxxdコマンドなどで確認可能です。

$ ./hoge.py > fuga
$ xxd fuga
0000000: 0a08 4a6f 686e 2044 6f65 10d2 091a 106a  ..John Doe.....j
0000010: 646f 6540 6578 616d 706c 652e 636f 6d22  doe@example.com"
0000020: 0c0a 0835 3535 2d34 3332 3110 01         ...555-4321..

https://github.com/google/protobuf/tree/master/ruby

$ sudo gem install google-protobuf
$ protoc --ruby_out=src addressbook.proto

Rubyスクリプトはこんな感じで

#!/usr/bin/env ruby
require 'google/protobuf'
require './addressbook_pb'

# person = Tutorial::Person.new()
# person.id = 1234
# person.name = "John Doe"
# person.email = "jdoe@example.com"
# phone = Tutorial::Person::PhoneNumber.new
# phone.number = "555-4321"
# phone.type = Tutorial::Person::PhoneType::HOME
# person.phone.push(phone)

STDIN.binmode
person = Tutorial::Person.decode(STDIN.read)
puts Tutorial::Person.encode_json(person)

あとはPythonの出力をRubyの入力に食わせるだけ

$ ./hoge.py | ./hoge.rb
{"name":"John Doe","id":1234,"email":"jdoe@example.com","phone":[{"number":"555-4321","type":"HOME"}]}

こんな感じで、同一のメッセージ定義からそれぞれの言語でコンパイルして、異なる言語間でメッセージをやり取りすることが出来ました。今回は同一ホストの入出力を使いましたが、異なるホスト間でネットワークを介してAPIでメッセージ交換をすることも可能ですし、有用そうです。

実際どれくらいサイズメリット、速度メリットがあるのか

以下のJSONで記述可能な構造体に対して確認してみました。

{
  "name": "John Doe",
  "id": 1234,
  "email": "jdoe@example.com",
  "phone": [
    {
      "number": "555-4321",
      "type": "HOME"
    }
  ]
}

まずはサイズメリット

$ ls -lh bin_out json
-rw-r--r--  1 mtajitsu  staff    45B  9 21 00:24 bin_out
-rw-r--r--  1 mtajitsu  staff   103B  9 21 00:24 json

2倍以上差がつきました。

性能差に関しては以下のスクリプトで検証しました。

#!/usr/bin/env ruby

require 'google/protobuf'
require './addressbook_pb'
require 'benchmark'
require 'json'


STDIN.binmode
bin_body = STDIN.read
# protocolbuffersで予め出力したファイルを標準入力に食わせる
result = Benchmark.realtime do
  10_000_000.times do
    Tutorial::Person.decode(bin_body)
  end
end

puts "ProtocolBuffers #{result}s"

json_str = '{"name":"John Doe","id":1234,"email":"jdoe@example.com","phone":[{"number":"555-4321","type":"HOME"}]}'
result = Benchmark.realtime do
  10_000_000.times do
    JSON.parse(json_str)
  end
end

puts "JSON #{result}s"

出力結果はこんな感じで、2倍くらいの性能差が出ています。

$ ./speed.rb < bin_out
ProtocolBuffers 41.555945s
JSON 82.674674s

参考URL

このエントリーをはてなブックマークに追加