< :前の番号
^ :番号順リスト
> :次の番号
P :前の記事(スレッド移動)
N :次の記事(スレッド移動)
|<:前のスレッド
>|:次のスレッド
^ :返事先
_:自分への返事
>:同じ返事先を持つ記事(前)
<:同じ返事先を持つ記事(後)
---:分割してスレッド表示、再表示
| :分割して(縦)スレッド表示、再表示
~ :スレッドのフレーム消去
.:インデックス
..:インデックスのインデックス
# 概要
Ruby インタプリタのデータ構造についての協力者を募集します。具体的には、
特定のテーブルデータ構造の高速化、およびベンチマークの作成です。題目にコ
ンテストと書いていますが、とくに賞品はありません。
# 詳細
Ruby インタプリタでは、テーブルデータ構造を使っています。テーブルデータ
構造だと曖昧ですが、Ruby だと Hash クラスに代表される、あるキーに対して
特定の値が返る、というデータ構造と考えてください。キーに重複はないことと
します。
Ruby インタプリタのテーブルデータ構造には、st というデータ構造が利用され
ており、似たようなデータ構造はすべてこの st によって管理されています。st
は、非常に高機能で、一般化されたもので、具体的にはチェインハッシュ構造に
なっています。一般化するために、各操作を関数ポインタで渡すような形になっ
ています。詳細は RHG のこちら:
http://i.loveruby.net/ja/rhg/book/name.html
でも、RHG の時代よりも、もっと高機能になっていて、具体的には順序を付ける
ために、各エントリに双方向ポインタを持つようになっています。
さて、一般化された形の st は、使い勝手が良いのですが、「順序は気にしな
い」とか、「特定の値の組に特化する」と、さらに効率の良いデータ構造を作る
ことができます。Ruby インタプリタ内では、ID -> VALUE という、数値 -> 何
か、という組み合わせがよく利用されます。具体的には、メソッドテーブル(メ
ソッド名 -> メソッドの実体)とか、インスタンス変数の管理(@foo とかの名
前 -> インスタンス変数の値)とかです。
ID は、何か数値だと思ってください。
http://i.loveruby.net/ja/rhg/book/object.html によると、「このIDは任意の
文字列と一対一対応する整数だ」ということです。メソッド名とかに番号を振っ
たものとお考えください。
VALUE は、Ruby オブジェクトを表すものですが、void * として考えて貰っても
良いと思います(そう使っているところもあります、というか、現状はそうとし
か使っていなかった)。
なお、少し細かいことをいうと、今回はキーに ID 直接ではなく、各IDに連番で
番号を振っており、その番号を使っています。ID -> 番号、番号 -> ID という
のは簡単に交換可能です。
さっき、Ruby インタプリタの trunk に、この「ID -> VALUE」に特化した、
struct rb_id_table というデータ構造を取り込みました。導入の詳細は
https://bugs.ruby-lang.org/issues/11420 をご覧下さい。
インターフェース
https://github.com/ruby/ruby/blob/trunk/id_table.h
実装
https://github.com/ruby/ruby/blob/trunk/id_table.c
まぁ、細かいことは良いとして、ある ID(数値)を入れると、登録済みの値が
返ってくるものであればよく、それが効率が良いと嬉しいわけです。
現状、このテーブルの実装として、おおまかに分けて4つ実装しています。
(1) これまで通り st を使う
(2) 配列を使う
(2-1) ただの配列を使う
(2-2) ソート済みの配列を使う
(3) ハッシュデータ構造を使う
(4) 配列とハッシュデータ構造を混ぜる(要素数が小さい時には配列を使う)
デフォルトでは、(4) を使うようになっています。それぞれの特質について、少
しまとめます(格納している要素数を N とします)。データ構造の教科書に書
いてありそうな話です。
(1) st を使うのは、今まで動いていたからちゃんと動くという確信が
あります。ただ、色々冗長です。
(2-1) ただの配列
挿入:何も考えないので、実は他の構造に比べて一番速いです。
探索:要素数が少なければ、線形探索で高速に要素を発見出来ます。
要素数が多いと、線形探索は遅いです(O(N))。
削除:線形探索が必要です(O(N))。
(2-2) ソート済みの配列
挿入:ソートを気にしないといけないので若干遅いです(O(N))。
探索:バイナリサーチを使うことができるので速いです(O(log N))。
削除:削除箇所はバイナリサーチを使うことができるのですが、
削除後にソートを気にしないといけないので若干遅いです(O(N))。
ただし、削除マークを付けるだけの実装なら、O(log N)。
(3) ハッシュ(議論を簡単しています。本当はもっと複雑)
挿入:速いです(O(1))
探索:速いです(O(1))
削除:速いです(O(1))
ただし、どれも十分な空きが無いと遅くなります。
運が悪いと遅いです。
また、最悪実行時間の見積もりが困難です。
N が十分小さい時、たとえば、探索対象が L1 キャッシュに乗っている時には、
余計なことをしない線形探索が一番速いです。メモリ効率は配列で管理するのが
一番有利です。
配列については、よくテーブルは struct { key; value } みたいな構造を並べ
ますが、今回は探索時にキャッシュをヒットさせるために、キーだけ、値だけ、
にまとめています。なので、qsort 関数が使えなくて面倒くさかった。
とりあえず、現状作っているのはこんな感じですが、もっと改良する点もありそ
うな気がします。
対象とする、ID → 値のテーブルは、メソッドテーブルを考えてみると、
- あまり沢山の要素を持たないことが多い
- 多分、あまり削除しない
- 多分、追加は一度に沢山行なうことが多い(クラスによるメソッド定義など)
- 多分、探索が多い
という特徴があるんじゃないかなぁ、と思います(インスタンス変数とかも、同
じような感じじゃないかなぁ)。もちろん、これは Ruby アプリケーションに
よっても違うし、人為的にそうじゃないケースを作るのは容易です。なので、最
悪時もそこそこ動くと嬉しいです。
# お願い
21世紀にもなって、自分でデータ構造を実装するのもどうかと思うのですが、と
りあえずこんな課題があるので、誰かぼくのかんがえたさいきょうのテーブル
データ構造を考えてみませんか。
あと、優劣を比較するためのベンチマークが不足しています。もちろん、Ruby
インタプリタに組み込んでいるので、Ruby インタプリタ上の Ruby プログラム
で速度を比較してもいいのですが、他のオーバヘッドが大きすぎて、テーブルだ
けの速度比較がやりづらいところがあります。そこで、このテーブルデータ構造
だけで比較するものがあると嬉しいなと思います。自分で書けばいいのですが、
面倒くさくて。
興味がある方がいらっしゃいましたら、何か考えてみてください。
--
// SASADA Koichi at atdot dot net