ファイルI/Oの実装として、ファイルの内容に同期したメモリ上の領域にアクセスするメモリマップI/Oを利用する際のインターフェイスについて考察する。
スキップデータベースの検索の記事でも述べたが、通常のファイルI/Oだと、読み込むべきデータの長さが事前にわからない場合に効率が悪い。適当な長さを投機的に読み込んで、足りなければバッファを大きくして読み直すといった手順が必要になってしまう。しかし、ファイルの中身をメモリ領域としてアクセスできるメモリマップI/Oを使っているのであれば、そもそも読み込む(=データをバッファにコピーする)という操作が必要ないのだ。もうメモリに読み込まれているのだから。メモリマップは、UNIXならmmapシステムコール、Win32ならMapViewOfFileシステムコールとして実装されている。ならば、それを前提とした効率的なAPIが設計できそうだ。それが以下に定義するゾーンI/Oというインターフェイスである。
class Zone { public: virtual ~Zone() {} virtual int64_t Offset() const = 0; virtual char* Pointer() const = 0; virtual size_t Size() const = 0; }; class ZoneIO { public: virtual Status MakeZone( bool writable, int64_t off, size_t size, std::unique_ptr<Zone>* zone) = 0; }; class MemoryMapParallelFile : public File, public ZoneIO { ... };
ZoneIOインターフェイスを継承したFileの具象クラスは、ゾーンI/Oを行うべく、MakeZoneメソッドを実装する。書き込みの可否とファイル内のオフセットとサイズを渡すと、Zoneクラスのオブジェクトを生成して、そのポインタを引数zoneに設定してくれる。ゾーンオブジェクトを生成する際には、ファイル内の指定したオフセットから指定したサイズ分だけのメモリ領域を確保して、それを利用可能な状態にしてくれる。オフセットにサイズを足した値よりファイルサイズが小さい場合には、ファイルの大きさを増やしてくれる。また、オフセットに負数を指定した場合、ファイルの末尾にアトミックに領域を確保してくれる。このインターフェイスだけで、ReadもWriteもAppendも統合できてしまうのだ。
それって単にマップされたメモリのアドレスにオフセットを足した値を返しているだけではないかと君は言うかもしれない。生ポインタを公開すれば、ゾーンなんて仰々しい名前は要らないじゃないかと。しかし、そんなに甘くはない。マップされたメモリ領域は、別のスレッドによっていつ再確保されるかわからないのだ。もし再確保された場合、生ポインタを使ってアクセスをしていれば、確実にsegmentation faultを食らう。ゾーンは、その時点のその位置のポインタを提供するだけではなく、そのゾーンが生存している間はメモリの再確保が行われないようにするロックの管理もしているのだ。コンストラクタでファイルの共有ロックをかけ、デストラクタでその共有ロックを解放する。メモリの再確保は同じreader-writerロックに占有ロックをかけて行われる。よって、ファイルの通常の読み書きは並列に行うことができ、メモリの再確保を伴うファイルの書き込みだけが他の操作をブロックする。
さて、ゾーンI/Oを使うと、もはやサイズを指定してデータを読み込む必要がなくなる。あるオフセットからデータを読み込みたいと考えたなら、そのオフセットから末尾まで豪快にゾーンを作ってしまえばいい。そしてゾーン内でデータのデシリアライズをしてレコードオブジェクトを構築すれば、無駄なデータコピーをなくすことができる。これを使うと、ハッシュデータベースも、ツリーデータベースも、スキップデータベースも、全て高速化する。その反面、それを前提として実装してしまうと、メモリマップができないストレージの上にはデータベースを構築できなくなってしまう。なので、当面は、ゾーンI/Oはファイル層のReadとWriteとAppendを実装するためのみに利用して、データベース層ではあくまでそれらRead/Write/Appendのみを利用するようにしよう。もし誰かとガチ性能勝負をしなきゃいけない時が来たら、この最適化を行うことになるだろう。
ただね、私が言うのもなんだけど、DBMがそんなに早い必要ありますかね。バッファのコピーとかいうレベルの最適化は、ファイルシステムのキャッシュ上でデータが展開できている規模でのみ意味を持つ。それを過ぎると純粋にストレージデバイスとファイルシステムのベンチマークテストをしているだけになってくるので、ユーザランドの最適化がどうだろうとスループットは大して変わらなくなってくる。適切なアルゴリズムを選択する方が重要だ。なので、性能に最適な実装はメンテナンス性に弊害のない範囲で最初からすべきだが、それ以上の早まった最適化はしない方がいい。