Protocol Buffersで巨大なコレクションのI/O


## 問題
巨大なコレクション（10^6以上）のコレクションをProtocol Buffersで扱いたい。
C++のAPIのデフォルトの設定の場合、67MB以上のファイルを読み込む事ができない。

例えば、以下のsample_read.cppで巨大なファイルを読み込もうとした場合、

```cpp:sample_read.cpp
#include <iostream>
#include <fstream>
#include <string>
#include "person.pb.h"

int main() {

  GOOGLE_PROTOBUF_VERIFY_VERSION;

  MySample::Persons persons;

  std::ifstream fin("persons.pb", std::ios::binary);
  bool succeeded = persons.ParseFromIstream(&fin);
  if( !succeeded ) { std::cerr << "Failed to parse file" << std::endl; }

  for( int i=0; i < persons.person_size(); i++) {
    MySample::Person person = persons.person(i);
  }

  return 0;
}
```

読み込みに失敗するというメッセージが出力される。

```
[libprotobuf WARNING google/protobuf/io/coded_stream.cc:505] Reading dangerously large protocol message.  If the message turns out to be larger than 67108864 bytes, parsing will be halted for security reasons.  To increase the limit (or to disable these warnings), see CodedInputStream::SetTotalBytesLimit() in google/protobuf/io/coded_stream.h.
[libprotobuf ERROR google/protobuf/io/coded_stream.cc:171] A protocol message was rejected because it was too big (more than 67108864 bytes).  To increase the limit (or to disable these warnings), see CodedInputStream::SetTotalBytesLimit() in google/protobuf/io/coded_stream.h.
[libprotobuf WARNING google/protobuf/io/coded_stream.cc:78] The total number of bytes read was 67108864
Failed to parse file
```

そもそもProtocol buffersでは巨大なメッセージを一度に出力することは推奨されていない。
[Techniques](https://developers.google.com/protocol-buffers/docs/techniques) の"Large Data Sets"の項目を参照。
巨大なデータ中のそれぞれの部分についてはPBでシリアライズし、それを保存する方法はユーザーにまかされている。（シンプルなリスト、DBに入れるなど）


## 解決方法

いろいろと調べた結果、巨大なコレクションを扱うときには"Length-prefix framing"という手法を使うのが良さそう。
http://eli.thegreenplace.net/2011/08/02/length-prefix-framing-for-protocol-buffers/

これは各メッセージのサイズを先頭の４バイト（４バイトである必要は無いが、４バイトを超えることは無いだろう）に保存しておき、そのあとPBでシリアライズしたデータを読むという手法である。

つまり `[データサイズ][データ][データサイズ][データ]` というのが連続して並ぶ事になる。
データサイズは何バイトでエンディアンは何を使うかということは事前に決めておく必要がある。

４バイトでリトルエンディアンでデータサイズを書き出す場合、サンプルコードは以下のようになる。

```sample_length_prefix.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/io/coded_stream.h>
#include "person.pb.h"

int main() {

  GOOGLE_PROTOBUF_VERIFY_VERSION;

  std::ofstream fout("persons_lp.pb", std::ios::binary);
  google::protobuf::io::OstreamOutputStream oos(&fout);
  google::protobuf::io::CodedOutputStream codedOutput(&oos);

  const int max = 10000000;
  for( int i=0; i < max; i++) {
    MySample::Person person;
    person.set_id(i);
    std::ostringstream oss;
    oss << "foobar" << i;
    person.set_name(oss.str());
    oss << "@example.com";
    person.set_email(oss.str());
    codedOutput.WriteLittleEndian32(person.ByteSize());
    person.SerializeToCodedStream(&codedOutput);
  }

  return 0;
}
```

これをRubyでロードする場合は下の通り。メモリもあまり無尽蔵には使用していないようだ。

```load_length_prefix.rb
require 'pp'
require_relative 'person.pb'

input = File.open("persons_lp.pb")
person = MySample::Person.new

until input.eof?
  size = input.read(4).unpack("l*")[0]  # read 4 byte and read it as little endian 4-byte integer
  person.parse_from_string( input.read(size) )
  pp person if ( person.id.to_i % 100000 == 0 )
end
```

## 参考
http://eli.thegreenplace.net/2011/08/02/length-prefix-framing-for-protocol-buffers/
http://stackoverflow.com/questions/11640864/length-prefix-for-protobuf-messages-in-c
http://stackoverflow.com/questions/2340730/are-there-c-equivalents-for-the-protocol-buffers-delimited-i-o-functions-in-ja