ひとり勉強会

ひとり楽しく勉強会

レジスタマシンとスタックマシン

こちらで見かけた話題・・・

LuaVM は 4.0 でのスタックマシンから 5.0 でレジスタマシンに変更されたらしく、その時の理由が Lua: papers の "The implementation of Lua 5.0" の 第 7 節 にいくつか書いてありました。そこで書いてあることには・・・、の前に Lua 5.0 の「レジスタ」の仕様を簡単にまとめるておくと、こんな風になってます。

  • レジスタは最大 256 本
  • ローカル変数1つにつきレジスタ1本割り当てる
    • 高度なレジスタ割り当ては一切ナシ
    • 実機のレジスタに対応づけることも考えられていない
    • ローカル変数が 200 個以上ある関数を定義しようとするとエラー
  • 関数呼び出し時の引数渡し&レジスタ退避は、レジスタウインドウ方式
    • IA64 (さわったことないけど) や Sparc みたいな
    • レジスタフレーム」がスタック状に管理されてて call/return の時にベースポインタをずらして push/pop する感じ

富豪的です。その分、レジスタ割り当てを考える必要がないので、コンパイラのコード生成が大変/実装が難しい、ということはないです。なにせ構文木をそのまま深さ優先探索でたどるだけでコードが出てきます。引数の問題、状態の保存の処理は call と return 命令に全部集約されているので、直接スタックを操作する命令も不要です。とゆーか、ジャンプ時のスタック長の整合性とか考えなくていい分スタックマシンより実装楽かもという気もします。

話を戻して、"The implementation of Lua 5.0" に書いてあったレジスタマシン化の利点ですが、

レジスタマシンはコピーが少ない

"a = b + c" という式を考えると、'a','b','c' をそれぞれの変数のローカル変数番号としたとき、スタックマシンだと

getlocal 'b'  # 'b'の値をスタックにpush
getlocal 'c'  # 'c'の値をスタックにpush
add           # スタックポインタ2つ減らして和を push
setlocal 'a'  # スタックトップの値をpopして'a'に書き込み

こう、都合 3 回か 4 回かのコピーが発生しますが、レジスタなら

add 'a' 'b' 'c'

1 回。そして、特に Lua の場合、変数の「値」は型を表すタグ(4Byte)とポインタかdoubleのunion(8Byte)で計12Byteもある構造体なので、あんまり何回もコピーしたくないという事情があるそうな。「ポインタの下位ビットを利用してタグを云々〜」みたいなトリッキーな技を避けたいと思うと動的な言語の VM は同じ問題を抱えがち。

レジスタマシンの命令は word align しやすい

要は命令を固定長にしやすい。スタックマシンだと、「コードサイズが小さい」利点を保つために命令が可変長になってしまいがち(add 命令は 1 Byte で済むけど、getlocal や jmp 命令、定数ロード命令等々は、ローカル変数インデックスやジャンプ距離のように余計な情報がいるので 1 Byte では足りない)

そもそもコードサイズそんな大きくならないよ

オペランドの明示が不要なのでスタックマシンはコードサイズが小さい」と時々言われるけれども、そうでもないよ、という。上の "a = b + c" の例は極端ですが、エンコード方式にもよると思いますがスタックマシンだと4〜7Byte、レジスタマシンなら4Byte。大きな式のコンパイルはスタックマシンが勝つけれど小さめの式文を並べたプログラムだと大差ないんじゃないかなーというところでしょうか。

_

ここまでの話は、一般の「レジスタマシン」に当てはまるものではないと思いますが、Lua の場合こんなんでした。第 8 節に比較ベンチマークが載ってて、10% から最大で 100% くらい高速化してるとされています。実マシンのレジスタやキャッシュを完全に模倣するとなるとかなり頑張る必要がありますが、スタックマシン並に簡単に実装できる(個人的にはむしろスタックマシンより楽)範囲の富豪的レジスタマシンでも、速度差はそこそこ得られるらしいですよーというお話でした。