概要
RustのHashMapをRubyから使えるようにしてみたが…… の続きです。
前回、以下のように書いた。
ハッシュ値を取得するのに、Rubyのメソッドが動的に呼び出され、Rustのhash関数も実行されている。
さらに Eqの時もRubyのメソッドが動的に呼び出されている。
Rubyからメソッド呼び出しする場合は普通はメソッドをキャッシュしておくので次の呼び出しは速くなるが、CやRustから呼ばれる場合は毎回メソッドテーブルから探しに行く必要があって、とても遅い。
今回、メソッドをキャッシュさせることで find, insert, delete が速くなることを確認できた。
結果
before:
$ ruby benchmark.rb --rust --seed 1
Hash: Rust
Seed: 1
user system total real
values 1.896433 0.152249 2.048682 ( 2.049503)
keys 1.589012 0.118299 1.707311 ( 1.708012)
find 11.612382 0.002426 11.614808 ( 11.617854)
insert 15.397090 0.012138 15.409228 ( 15.413532)
delete 7.581418 0.001750 7.583168 ( 7.585473)
after:
$ ruby benchmark.rb --rust --seed 1
Hash: Rust
Seed: 1
user system total real
values 1.924479 0.166750 2.091229 ( 2.092445)
keys 1.514441 0.112787 1.627228 ( 1.628187)
find 8.677083 0.001990 8.679073 ( 8.681669)
insert 12.661791 0.010463 12.672254 ( 12.678383)
delete 5.908580 0.001670 5.910250 ( 5.912530)
insertは標準のHashクラスより37%も速くなっている。やったね
やったこと
これまで、hash関数は以下のように実装していた。
fn hash<H: Hasher>(&self, state: &mut H) {
let val = ruby::fun_call(self.0, "hash", &[]);
val.to_raw().hash(state);
}
Rustから呼び出すと毎回テーブルを引く必要があるが、Rubyから呼び出せばいい感じにキャッシュされる。
つまりRubyを経由して呼び出せばよい。というわけで
pub static mut M_HASH: Value = NIL;
// ...
let sym_hash = module_eval(klass, "def self.value_hash(val); val.hash; end");
M_HASH = obj_method_by_symbol(klass, sym_hash);
eval
を作ってRubyの関数を定義する。
fn hash<H: Hasher>(&self, state: &mut H) {
let method = unsafe { crate::hashmap::M_HASH };
let val = ruby::method_call(method, &[self.0]);
val.to_raw().hash(state);
}
hash関数では、インスタンスメソッドを呼ぶ代わりにこのキャッシュされた Method
呼び出しを行う。
extern "C" fn mark(ptr: *const ffi::c_void) {
gc_mark(unsafe { M_HASH });
この Method
のインスタンスがGCで回収されないように注意(これが原因で時々SEGVが出てデバッグが辛かった)