何をしたいか
バイナリを解析するときにもっとも高速に数値に変換したい。
ランダムバイナリ文字列の特定の場所にある数値を 32bit signed integer にリトルエンディアンで変換する場合の高速な手法を探す。
もっといえば、unpack は本当に速いか。
ここでは 100MB のランダムバイナリを作り、そこからなるべく速く数値を 1000 万回選び出す。
立候補者
bytes さん
bytes でデータ全体をバイト列に変換したあと、pack して、さらに unpack する。泡沫候補。
data.bytes[e, 4].pack('C*').unpack('l<')[0]
byteslice さん
byteslice でデータからバイナリを切り出したあと、unpack する。最有力候補。
data.byteslice(e, 4).unpack('l<')[0]
@ さん
unpack の @ を使って位置指定する。位置はテンプレート文字列で与える(to_s は遅い)。こちらも有力候補。
data.unpack("#@#{e}l<")[0]
Fiddle さん
位置指定をポインタでやってみる。危険かつひねくれ者。ポインタの演算をするとそのタイミングでクラスを作るのが厄介。
require 'fiddle'
ptr = Fiddle::Pointer[data]
(ptr + e).to_str(4).unpack('l<')[0]
bit-shift さん
bytes でバイト列にしたあとそのバイト列をビットシフトして値を作り直す。新勢力になるかもしれない。
a, b, c, d = data.byteslice(e, 4).bytes
r = a + (b << 8) + (c << 16) + (d << 24)
r = (r >= 2 ** 31 ? r - 2 ** 32 : r)
bin_utils さん
bin_utils という gem を使う。ネイティブなので速いかもしれない。
BinUtils.get_sint32_le(data, e)
rubyinline さん
rubyinline という gem を使う。こちらもネイティブ関数を作れる。ビット長とエンディアンが環境依存なので注意する必要がある。
require 'inline'
class Object
inline do |builder|
builder.c "
static int native_unpack(char *str, int pos) {
return *(int*)(str + pos);
}
"
end
end
native_unpack(data, e)
実験と考察
実験条件はいかのとおり。
require 'benchmark'
prng = Random.new(0)
data = prng.bytes(1024 * 1024 * 100)
smpl = (0...10000000).map{ prng.rand(1024 * 1024 * 25) * 4 }
bytes はあまりに遅い(10 個でも数秒かかる)ので省いた。
結果は以下の通り。上から byteslice、@、Fiddle、bit-shift、bin_utils、rubyinline の順。一番下は byteslice のみを行った場合。
user system total real
4.310000 0.020000 4.330000 ( 4.349936)
6.680000 0.030000 6.710000 ( 6.745377)
7.190000 0.040000 7.230000 ( 7.269277)
7.980000 0.040000 8.020000 ( 8.079712)
1.620000 0.010000 1.630000 ( 1.634057)
1.540000 0.010000 1.550000 ( 1.558589)
2.210000 0.010000 2.220000 ( 2.237148)
@ 指定は文字列に変換する処理の分遅くなっていると考えられる気がする。Fiddle もクラスベースじゃなければな。
ビットシフトはわりといけると思ったんだけどそんなことはなかった。というかこれがやりたかっただけ。
bin_utils 強すぎ。byteslice している暇すら与えられなかった。
rubyinline 最強。ネイティブは正義。
結論
環境依存があってもいいなら rubyinline、汎用性を求めるなら bin_utils を使いましょう。
おまけ
bin_utils では浮動小数点が実装されていないので、byteslice を使うのがいいと思う。
# リトルエンディアン・単精度
data.byteslice(e, 4).unpack('e')[0]
# ビッグエンディアン・倍精度
data.byteslice(e, 8).unpack('G')[0]
任意長 Null 終端文字列の場合は byteslice できないので、@ で切るのがいいと思う。Fiddle でもできるが強くおすすめする理由はない。
# unpack
data.unpack("@#{e}Z*")
# Fiddle
ptr = Fiddle::Pointer[data]
(ptr + e).to_s