デシリアライズ速度の比較 ByteBuffer vs DirectBuffer vs Unsafe vs C

OpenJDK や Hotspot VM には sun.misc.Unsafe という内部APIがあり*1、これを使うと ByteBuffer.getInt や ByteBuffer.getLong よりも高速にバイト列から整数値をデコードできるという。これを駆使することで、Cで実装された拡張ライブラリに匹敵する速度を出せるらしい。

それが本当なら、データ圧縮ハッシュ関数、シリアライザ/デシリアライザなどの実装を高速化できる。例えば、lz4xxhashJava実装が Unsafe API を使用している*2jpountz/lz4-java
Prestoも、中間データのシリアライズ/デシリアライズにはすべて Unsafe API を使っている*3

そこで、実際にベンチマークしてみた。

ベンチマーク内容

  • 10MBのランダムなバイト列を生成する
  • 先頭から1バイト読み出す
  • その1バイトの先頭ビットが1なら:32bitの整数をbig endianで読み出す
  • その1バイトの先頭ビットが0なら:64bitの整数をbig endianで読み出す
  • データの末尾に達するまでループする

結果

f:id:viver:20140312003334p:plain

  • ByteBuffer heap: new byte[…]で確保したメモリを、ByteBuffer.wrapして整数値をデコード
  • ByteBuffer direct: ByteBuffer.allocateDirectで確保したメモリから、整数値をデコード
  • Unsafe heap: new byte[…]で確保したメモリから、Unsafe APIを使って整数値をデコード
  • Unsafe direct: ByteBuffer.allocateDirectで確保したメモリから、Unsafe APIを使って整数値をデコード
  • C shift: ビットシフトを使って、バイト列から整数値をデコード
  • C load: memcpyとエンディアン変換関数(bswap_64、__DARWIN_OSSwapInt64、_byteswap_uint64など)を使って、バイト列から整数値をデコード

確かにUnsafeを使うとCに匹敵する性能が出る。これは予想以上に速い。
一方でヒープメモリからのByteBuffer.getIntは、かなり遅いことが分かる。Unsafe APIではヒープメモリでもdirect bufferでも差はないので、ヒープメモリからのByteBufferをUnsafeに変更すれば、2倍近く性能が向上することになる。

Cにはstrict-aliasingルールがあるので、バイト列から整数値をデコードするコードは複雑になる。コンパイラが賢いので冗長に見える割には速度が出るのだが、ポータブルで安全なコードを書くのは結構難しい。strict-aliasingルールについて詳しくは:

実行環境


===
後日追記:あわせて読みたい何故JVM(HotSpot)のUnsafe APIは速いのか

*1:Dalvikにあるかどうかは調査していない。誰か教えてください。

*2:ちなみにlz4とxxhashの開発者は同一人物。

*3:Prestoが依存している airlift/slice ライブラリがUnsafe APIをラップしたクラスを実装している。airliftを開発しているのはPrestoと同じメンバー。