RustのHashMapをRubyから使えるようにしてみたが……

RustのHashMapをRubyから使えるようにしてみた。
https://github.com/irxground/rust_hashmap_gem
RustのHashMapはかなり速度に力を入れているので、置き換えるだけで速くなったら面白いなーと思ってやってみた。

→ [もう少し速くしました](https://qiita.com/irxground/items/d40de964927d8f64b0a1)

## 結果

そんなわけで、[適当なベンチマーク](https://gist.github.com/wrzasa/6b456f73012ce98ae6feb6aaa4ba933e)を拾ってきて試してみた。insert以外全部負けてますね

```
$ ruby benchmark.rb --rust --seed 1
Hash: Rust
Seed: 1
       user     system      total        real
values  1.885191   0.164441   2.049632 (  2.052237)
keys    1.461595   0.054283   1.515878 (  1.516320)
find   11.256652   0.002255  11.258907 ( 11.261749)
insert 15.286804   0.013948  15.300752 ( 15.305879)
delete  7.310167   0.001280   7.311447 (  7.312999)
```

```
$ ruby benchmark.rb --seed 1
Hash: Original
Seed: 1
       user     system      total        real
values  1.100674   0.150917   1.251591 (  1.258886)
keys    0.526004   0.042145   0.568149 (  0.570534)
find    2.607541   0.002803   2.610344 (  2.616013)
insert 17.317054   0.012727  17.329781 ( 17.335842)
delete  3.717545   0.000869   3.718414 (  3.719866)
```

## 実装の話

RustのHashMapに格納するには `Eq` traitと `Hash` traitを実装する必要がある。
これはRubyも同じで、Hashのキーとして適切に扱うには `Object#hash` と `Object#eql?` を適切に実装する必要がある。
となると、Rubyと挙動を合わせるにはそのtraitをそのメソッドを使って実装する必要がある。
今回は `Hashable` という構造体で `VALUE` をラップして作ることにした。

## 感想

keys, values が遅いのはどうやらRubyのHashはそもそも内部的に配列で持っているらしい。であればどう頑張っても同じ速度にすることは難しそう。

```rust
extern "C" fn keys(self_: Value) -> Value {
    let map: &Map = unsafe { &*check_typeddata(self_, &RUBY_TYPE) };
    let ary = ary_new_capa(map.len());
    if map.len() > 0 {
        ary_store(ary, (map.len() - 1) as isize, NIL);
        let ptr = ary_ptr_use_start(ary);
        let slice = unsafe { std::slice::from_raw_parts_mut(ptr, map.len()) };
        for (item, (key, _)) in slice.iter_mut().zip(map.iter()) {
            *item = key.0;
        }
    }
    ary
}
```

keysの実装はこのようになっていて先に必要なサイズを持ったRubyの配列を作成し、先頭のポインタを受け取って値を順に格納しているのでこれ以上は速くならなさそう。

find, delete が遅いのは、おそらくオーバーヘッドが大きすぎるから。

```rust
extern "C" fn get(self_: Value, key: Value) -> Value {
    let map: &Map = unsafe { &*check_typeddata(self_, &RUBY_TYPE) };
    match map.get(&Hashable(key)) {
        Some(val) => *val,
        None => NIL,
    }
}
```
getの部分はこうなっていて、だれが書いてもこうなるしこれ以上速くはできなさそう。
問題はHashableの方で以下のように実装している。

```rust
use crate::ruby;
use std::hash::{Hash, Hasher};

#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct Hashable(pub ruby::Value);

impl PartialEq for Hashable {
    fn eq(&self, other: &Self) -> bool {
        let val = ruby::fun_call(self.0, "eql?", &[other.0]);
        val != ruby::FALSE
    }
}
impl Eq for Hashable {}

impl Hash for Hashable {
    fn hash<H: Hasher>(&self, state: &mut H) {
        let val = ruby::fun_call(self.0, "hash", &[]);
        val.to_raw().hash(state);
    }
}
```

ハッシュ値を取得するのに、Rubyのメソッドが動的に呼び出され、Rustのhash関数も実行されている。
さらに Eqの時もRubyのメソッドが動的に呼び出されている。
Rubyからメソッド呼び出しする場合は普通はメソッドをキャッシュしておくので次の呼び出しは速くなるが、CやRustから呼ばれる場合は毎回メソッドテーブルから探しに行く必要があって、とても遅い。

Rustの場合はHasherに何を選ぶかでHashMapのパフォーマンスが変わるくらいhashアルゴリズムは重要なので、これだけのことをしていたらこれだけ遅いのもおかしくない。逆に insert はこのオーバーヘッドを払ってでも速いので、相当速い。

あとは、RustからRubyの関数を呼ぶ場合、共有ライブラリの関数を呼んでいるので関数の境界を越えた最適化（インライン化を含む）ができない。