連鎖性言語を作る9
VMの命令列にコンパイルして実行するようになったのでなんとなくVMっぽい気がする感じになったところで、命令が2個しかないのは物足りない。これを増やすことを考えてみよう。オレ言語はスタック指向であり、スタックマシンのVMとは相性がよい。相性がよいと言うか、言語がシンプルなせいでほぼそのままストレートに組み込みワードをVM命令化することができる。
実際のところ、コードジェネレータをこのようにして、
module CodeGenerator def self.generate(ast) ast.instance_variable_set(:@bytecode, ast.map{|d| case d when :swap Instruction.new(:swap, nil) when :drop Instruction.new(:drop, nil) when :dup Instruction.new(:dup, nil) when Symbol Instruction.new(:call, d) when OreSymbol Instruction.new(:push, d.sym) else Instruction.new(:push, d) end }) end end
VMをこのようにすれば
def run(ast) unless ast.instance_variable_defined?(:@bytecode) CodeGenerator.generate(ast) end ast.instance_variable_get(:@bytecode).each do |d| case d.insn when :swap @stack[-2], @stack[-1] = @stack[-1], @stack[-2] when :drop @stack.pop when :dup @stack << @stack[-1] when :call @words[d.data].call when :push @stack << d.data else raise end break if @break_flg end end
swap、drop、dupの3つの組み込みワードはVM命令化できる。じゃあこの調子で全部VM命令化してしまえばいいのかと言うとそうでもない。VMとかの専門家ではないのでイマイチだがちょっと考えてみる。
VMと命令セット
言語処理系にVMを実装すると、必然的にVMの命令セットが必要になる。ASTをVM命令列に変換して高速実行しようというのがVM化の目的なのでそりゃそうなのだが、VM命令セットにどのような命令を持たせるかがひとつの問題になる。
例えばオレVMはオレ言語に特化しているのでswapやdupがあると非常に良いが、汎用的に使えるVMを目指すなら他の言語ではそれらの命令におそらく使い道が無い。VMのディスパッチはどうせ間接ジャンプすることになるので命令数が多くても大して性能劣化は無いかもしれないが、多すぎると命令キャッシュ的に不利だし、メンテも大変だし、JIT化するのも辛いので、ある程度は厳選すべきだ。
機能のタイプ
VM命令セットに最適解は無いので、現実的にはほどほどに速くて扱いやすくてメモリに優しく高機能かつスマートで美しいものがよい。たぶん設計者の主観で決められる。命令セットに入らなかった言語機能は命令の組み合わせで実現される場合もあるし、組み込みメソッド的な形で実装される場合もある。そうでなければその言語自身で実装される。ある機能の実装位置についてはそれぞれ特性があるので、適当に選んでもいいが、それなりに理想的な場所があると思われる。
VM命令として実装すれば速いし、JIT化した場合にそのままネイティブ化して埋め込まれるコードになるだろう。組み込みメソッドは初めからネイティブコードになっていて速いが、呼び出しにオーバーヘッドが発生し、その言語自身の高速化の影響を受けない。その言語自身で書くと遅いがその言語自身が速くなれば同様に速くなる。
オレ言語はシンプルだが、現状組み込みワードとして作られている機能をすべてVM命令化するかというとさすがに違うよね、ってなる。とはいえじゃあどういう基準で選択するのが良いのかと言うと答えも特に持ち合わせていない。言語の設計・実装をする人たちは難しい問題と向き合っているのだな。