okuyamaのSerializeMapを検証してみた(Get編)(番外編MessagePack for Javaも試してみた)

前回の記事でokuyamaに追加予定の機能である、
SerializeMapというメモリの効率化を考えて実装してみたMap実装の
Set性能を図ってみましたが、Mapなので当然Getがいるので今回はその性能を計測してみます。


まず、その前にこのSerializeMapの構造を下手ですが絵にしてみました。



上記のような構造です。内部でConcurrentHashMapを1つだけ保持していて、
このMapのValueにさらにHashMapをもっています。このHashMapに実際にKeyとValue
セットが格納されます。そしてこのHashMapはSerializeした後に
圧縮してbyte型の配列でもっているので、実際にMapが格納されているわけではありません。
Set、Getなどの処理が走った場合は、処理依頼のKey値をハッシュ関数を使って数値にして、
それをConcurrentHashMapの要素数で割って出た余りの場所のHashMapのbyte配列をDserializeして
処理を行います。ConcurrentHashMapの要素数は初期化時に固定化するので、固定数を超えることはなく、
メモリ中のMapオブジェクトはConcurrentHashMap1つになります。


利用したPCは前回と全く同じ、JVMオプションも同じで、メモリ割り当てだけ1024MBにしました。


テスト用のPGMは以下です。
http://sourceforge.jp/projects/okuyama/svn/view/trunk/test/SerializeMapGetTest.java?view=markup&revision=684&root=okuyama
このPGMは事前に指定した件数だけ、要素を登録してそれに対して並列に指定スレッド数で、Get処理をランダムに行います。
今回のテストは、事前に300万件登録したところに、8スレッド並列Getを行うパターンと、16スレッド並列Getを行うパターン。
それと、事前登録件数を500万件に増やしたパターンの4パターンを試しました。


まず、ConcurrentHashMapをそのまま利用した場合の参考値です。
※メモリ割り当てが512MBでは初期の300万件が格納しきれないので、メモリだけ2048MBにしました。
ConcurrentHashMapのテスト結果
・300万件 - 8スレッド
 4208588 QPS


・300万件 - 16スレッド
 4263423 QPS




・500万件 - 8スレッド
 3333208 QPS


・500万件 - 16スレッド
 3360552 QPS


速いなー!!
300万件の時に400万QPS超えてるし。




次にSerializeMapのテスト結果です。


・300万件 - 8スレッド
 189167 QPS


・300万件 - 16スレッド
 191084 QPS




・500万件 - 8スレッド
 145988 QPS


・500万件 - 16スレッド
 144266 QPS




上記のような結果になりました。
ConcurrentHashMapと比べてしまうと、全然ですが、
一応20万QPSぐらい出ているので、そこそこのスピードは出るようです。
並列数でのスピード低下はあまりないようです。
当然同じ値にアクセスを繰り返せば遅くなりますが、ある程度ランダムにアクセスがある場合は大丈夫なようです。
それよりもデータ数の増加に影響を受けるようです。これは圧縮解除、Dserializeの部分で大きなデータを扱うからですね。




さてここからは番外編です。
シリアライズしてデータを持つということで、ここまでは(デ)シリアライザにJavaのObjectOutputStreamを利用していたのですが、
その他のものも試してみたいと思います、MessagePack for Javaをためさせていただきました。
バージョンは0.5.2を利用。
プログラムとしてはSerializeMapのシリアライズ処理とデシリアライズ処理の部分をmsgpack用に変更して、圧縮は既に
ライブラリ側で行っているという情報がありましたので除外。


シリアライズは以下のような構文
    public static byte dataSerialize(Map data) {
      return MessagePack.pack(data);
    }


デシリアライズは以下のような構文
    public static Map dataDeserialize(byte
data) {
      return (Map)MessagePack.unpack(data, tMap(TString, TString));
    }


ものすごく分かりやすいですね!!


では早速Setからテスト。
条件は[http://d.hatena.ne.jp/okuyamaoo/20110616:title=前回]とまったく同じ状態です。


・1秒当たりのセット数
TotalExecCount = 2981566
QPS = 298156
凄いです。秒間30万Setなので、標準のObjectシリアライズが6万QPS程度だったので、
大体5倍程度のスピードです。単純にConcurrentHashMapと比べて、2分の1程度のスピードが出ています。




つぎに、限界格納数


開始直後
1124725
2112027
2788948
3377631
3938942
大体、3秒で100万から70万件のペースでsizeが増えているので、
30〜25万QPSでているのが分かります。
非常に高速ですね。


そして、1000万を超えた当たりでは、
10337128
10462071
10597972
10760863
10907505
大体、3秒で13万件から15万件程度増えているので、5万QPSぐらでしょうか。
引き続きかなり高速です。


そして、1400万件程度のデータを格納した状態で、秒間のSet数が700QPS程度になりました。
14685807
14688025
14690492
14692575
14694516


この後、1480万程度でOutOfMemoryとなりました。
ConcurrentHashMapをただ利用した場合は200万件程度でOutOfMemoryでしたので、
大体7倍程度のデータが保持できて且つ、1000万件保持時点でもかなり高速です。


続いて、Getのテストです。
こちらも最初のほうに書いているのと同じ条件で試しました。
テスト結果です。


・300万件 - 8スレッド
 972738 QPS


・300万件 - 16スレッド
 963889 QPS




・500万件 - 8スレッド
 683249 QPS


・500万件 - 16スレッド
 684657 QPS




凄く速い!!
データの増加で遅くなることはどうしようもないとは思いますが、
300万件へのアクセスにいたっては、100万QPSに届きそうな勢いなので、
実にObjectOutputStreamの5倍です。500万件でも非常に高速であることが分かりますが。




シリアライズによりデータサイズがかなり小さくなり、そして高速なシリアライズ、デシリアライズは非常に有効ですね。
okuyamaでも利用させて頂こうかと考えております。