書いた人:ささだ
YARV: Yet Another RubyVM の中を解説するこの連載。今回は前号の続きで YARV の命令セットを紹介します。
前号では、Ruby プログラムがどのような YARV の命令列に変換されたかを、とても簡単な例で示しました。今回も、簡単な例を続けます。
YARV をスレッドの実行に対応しました。つまり、 Thread.new{ 何か } と書くと別スレッドで「何か」が動きます。といっても、まだまだ不完全ですが。とくに I/O やシグナルまわり。そのあたりを触らないように、いじってもらえると幸いです。
現在の Ruby のスレッドは OS(など)が提供するネイティブスレッドを使わず、すべてユーザレベルで実現されています(俗にいうグリーンスレッドモデル)。そこで、YARV ではネイティブスレッドを使うようにしました。現在は pthread と Windows のスレッド環境に対応しています。
このあたりの話はいろいろあるのですが(何を隠そう、私の大学の卒論テーマはスレッドライブラリの開発です)、小難しくなるのでやめ。また、ネイティブスレッドを利用するにしても、どんな設計にするのか、選択肢とトレードオフがいろいろあります。そのあたりの話は [yarv-dev:631] Re: thread support から関連するスレッドに(試行錯誤の後として)まとまっています。興味のある方はこちらをどうぞ。
現在の YARV には、処理速度向上のためにいろいろな最適化の仕組みを実装しているのですが、それらはデフォルトではほとんど無効になっています。有効にして処理速度向上を実感したい方は、vm_opts.h というファイルを変更して有効にしてください。変更の仕方は、多分読めばわかると思います。今後は configure などでこの最適化の有無などを選択できるようにしたいと考えています。
デフォルトで最適化が無効なのは、最適化を有効にするとコンパイル速度がかなり遅くなってしまうというのが理由のひとつ。もうひとつの理由はこの連載で示している YARV 命令列と、最適化後に出力される(前回紹介した make parse で出力される)命令列が違うものになってしまうからです。
Ruby プログラムを YARV 命令列に変換して、それを逆アセンブルして表示する CGI を作りました(YARV - Compile and Disassemble CGI)。
この CGI を見てもらうと、おっきなテキストフィールドがあると思うので、そこに適当な Ruby プログラムを書き込んで、「compile and disassemble」というボタンを押してみてください。ずらずらと色々な情報、つまりこの連載でちょうど説明している YARV 命令列が表示されます。
もちろん、コンパイルして逆アセンブルするだけなので、YARV でそのプログラムを実行するわけじゃありません。そんな恐ろしいことは出来ません1。
いろいろ試して、Ruby プログラムがこんな命令に変換されるんだ、というのを見て楽しんでもらえればいいと思います。
ちなみに、もうひとつあるボタン「compile and disassemble (to optimized instructions)」は、コンパイル時に YARV 命令列を最適化して表示します。多分、見てもさっぱりわかんないんじゃないかと思います。この最適化された命令列の読み方は、いずれ紹介したいと思っています。
さて、前置きが長くなりましたが、今回は「リテラル・変数・定数」式を表現する YARV 命令列についての解説です。なぜ、このテーマを選んだかというと、とても簡単だからです。
具体的には、
とか、
とか、
とか、そういうのを、YARV 命令列ではどうやって表現するのか解説します。見るからに簡単そうですね。
Ruby でリテラルというと、true/false/nil などの固定の値、数値(1、12345678901234567890 、1.2)、シンボル(:Symbol)、文字列(”String”)、正規表現(/abc/)などがあります。また、似たようなものに配列([1, 2, 3])、ハッシュ({1=> “a”, 2=> “b”})、範囲(1..3)などの式があります。
YARV 命令列では、これらのリテラル式はスタックにそのリテラルが示すオブジェクトを積む、という操作になります。
変更できないオブジェクトを immutable なオブジェクトと言います2。たとえば、true オブジェクトはいつも true です。123 はいつも 123 です。内容が変わったりはしません。前述した例では、true/false/nil などの固定の値、数値(1、12345678901234567890 、1.2)、シンボル(:Symbol)、正規表現(/abc/)などがこれにあたります。
これらのリテラル式は putobject という命令で表現されます。
という Ruby プログラムは次のようにコンパイルされます。
setlocal というのは、「ローカル変数 a (命令列中では 2 と表現されている)に、そのリテラルの値を代入する」、という意味です。これについては簡単そうで難しいので次回説明します。end は前号でも紹介したとおり、そのスコープの終わり(そして呼び出し元、例えばメソッドコールの返り値としてスタックトップの値を返す)、という意味ですが、これもあわせて次回に。
setlocal、end を除くと、上記命令列には「putobject [何か]」という命令と、putnil という命令だけになります。putobject は、[何か] をスタックに積む、という意味で、putnil は nil をスタックに積む、という意味になります。putobject nil と書いてもまったく同様なのですが、putobject nil のパターンは大量に出てきて、コンパイラを書くときに面倒くさかったので一個命令を追加しました(今思えば、マクロにすればよかった)。
これは簡単ですね。
余談ですが、
という何もしない Ruby プログラムをパースするとどうなるでしょう。
何もしない YARV 命令列が生成されました3。
次は文字列リテラルです。文字列リテラルは破壊的な変更が可能なので mutable なオブジェクトと言えます。たとえば、プログラム上同じ位置にあっても、文字列リテラルが返すオブジェクトは毎回異なります4。
というわけで、毎回同じオブジェクトをスタックトップに置くだけの putobject 命令は使えないので、代わりに putstring 命令を使います。
というプログラムは、
とコンパイルされます。面倒なので、これからは setlocal 以下は載せないようにしますね。
とくに難しいことはありません。
配列やハッシュ式、範囲式で生成されるオブジェクトも mutable です。これらの式の特徴は、先ほどの例とは違って、他のオブジェクトを利用して作ることです。たとえば、配列オブジェクトを生成する [1, 2, 3] という式は、Fixnum のオブジェクト 1, 2, 3 を利用して作ります。
これらはどのようにするかというと……、例を見たほうが早いですね。
をコンパイルすると、
という命令列になります。つまり、スタックに “a”、”b”、”c” と積んで、最後に newarray 命令でスタックトップの 3 つ、つまり今積んだ “a”、”b”、”c” を取ってきて配列にする、という意味です。
では、要素が String ではなくて Fixnum である配列を見てみましょう。
このような Ruby プログラムを YARV 命令列にすると、次のようになります。
要素がすべてリテラル(putobject 命令で済むもの)だった場合、毎回各要素をスタックに積まなくても作るものが決まっているため、duparray 命令ひとつ(配列オブジェクトを dup してスタックトップに積む)で済むことになります。
ハッシュは配列の要素数を 2 倍(1 要素 key と value で 2 つ)にしただけです。newhash 命令になります。
範囲式は Range オブジェクトを生成します。これを行うための命令 newrange は先端と終端を示すオブジェクト 2 つをスタックトップから取り出し、新しい Range オブジェクトを生成します。
というプログラムは、
このような YARV 命令列になります。(x..y) の場合は newrange の命令オペランドに 0 を、(x…y) の場合は 1 を指定してあります。
さて、ここで
という、よくありがちな Range 式をコンパイルしてみましょう。
newrange 命令はありません。これは、Range オブジェクトは(多分) immutable なので、範囲を指定する式が二つとも immutable である場合はこのような最適化が可能なのです。
ここでは変数について説明します。ただ、ローカル変数は面倒くさいので次号にまわします。
YARV 命令として、インスタンス変数を設定したり、値を取得するための特別な命令を用意しています。
は次のような YARV 命令列になります。
set/getinstancevariable という命令があるだけです。とても簡単。
YARV 命令として、グローバル変数を設定したり、値を取得するための特別な命令を用意しています。
は次のような YARV 命令列になります。
set/getglobal という命令があるだけです。とても簡単。
YARV 命令として、クラス変数を設定したり、値を取得するための特別な命令を用意しています。
は次のような YARV 命令列になります。
set/getclassvariable という命令があるだけです。setclassvariable には true という命令オペランドが付いてますね。これはなんでしょうか[^5]。えーと、調べてみると警告を出すためにクラス変数定義の場所で true / false が変わったりするようなのですが、今 ruby 1.9 のソース(つまり、YARV のソース)を確認すると、この値を見てませんねぇ(汗) リファクタリング対象のようです。
Ruby の定数は不思議なことに、他の言語でいう定数ではありません。リフレクション機能を使ったり、警告を無視したりすれば簡単に再定義が可能です。
また、実行時に後述する定数検索を行わなければならないため、定数のくせにアクセスコストは定数ではありません(検索コストは検索パスの大きさに比例します)。そのため、一度検索したらその結果をキャッシュしておく__インラインキャッシュ__も用意していますが、それも後述します。
定数アクセスには次のように、いくつか種類があります。
そのため、ただ C としてアクセスしているのか、::C としてアクセスしているのかで、意味が全然違います。そこで、定数の値を得る(スタックに積む)命令 getconstant はスタックオペランドに定数検索の起点を置くことにしました。
定数を設定する setconstant 命令も同様に、スタックオペランドで設定する場所を決めます。
ちょっとあいまいな言葉が多かったりしますが、厳密に言い出すととても面倒なのでこの辺で。
定数の検索は、処理系実装者から見るとかなり作るのが面倒な部分になっています。ふつう気がつかないんだけど、細かい仕様がたくさんあるんです。詳細は RHG を読んでください(第 6 章 変数と定数、第 14 章 コンテキスト)。要は、コンパイル時には定数検索パスが決まらないのです。
この話題は、実行コンテキストの話も含めて、より細かい実装の段階で解説します。
さて、定数は定数という名前なのですから、再定義できるとはいっても、めったにそんなことは起こりません。そこで、一度値を検索したらキャッシュすることにしました。それを実現するのが get/setinlinecache 命令です。
getinlinecahce 命令は、まず自分自身が値をキャッシュしているかどうか確認しています。もしキャッシュしており、その値が使えたら位置 cache_end(setinlinecache 命令の後ろ)へジャンプします。そうでなければ、まず nil を積んでから[^6] [… 何か式 …] の部分を実行します。setinlinecache 命令は、位置 cache_start で示される getinlinecache 命令のキャッシュ領域(命令オペランド部分)に、[… 何か式 …] の計算結果をキャッシュします。
キャッシュした値が正しいかどうかは、定数を定義をすると増加するカウンタ値(これは VM グローバルなカウンタ)とともにキャッシュしておき、現在のそのカウンタ値とキャッシュしたときのカウンタ値を比べることでその値が使えるかどうか確認できます。
この命令は Ruby プログラムの意味の表現とは直接関係ありませんが、性能には結構影響します。
さて、これを踏まえて、次に示す定数アクセスプログラムの YARV 命令列へのコンパイル結果を見てみましょう。
次のようになります。
pop 命令はスタックからひとつ値を捨てる命令です。getinlinecache の命令オペランド <ic> は値をキャッシュする領域を示しています。
ちょっと長いですが、意味はそれぞれ単純なのでよく見ればわかると思います。
今回も YARV 命令セットの説明を行いました。リテラル・変数・定数という、Ruby でも簡単なところばかりだったので、退屈してしまったかもしれません。次回は、命令セットの続きとして、もうちょっと難しいところに踏み込んでみようと思います。お楽しみに。
ささだこういち。学生。
いつもこの時期になると英語に対する危機感を覚えるんだけど、すぐにのど元を過ぎてしまう。まぁいいか(いや、良くないよ!)。
loop{ p “String”.object_id # mutable / 毎回オブジェクト ID が変わる p true.object_id # immutable / オブジェクト ID は不変 p :Symbol.object_id # immutable / オブジェクト ID は不変 } というプログラムを動かすとわかります。 [^5]: 本稿執筆中、筆者はマジで忘れていた。 [^6]: nil を積むのは、getconstant 命令の出現箇所の多くで次の命令が putnil 命令であることが多いため。
バグが多すぎる、ということがばれるのが怖い、というわけじゃなくて、セキュリティの問題です。もちろん。 ↩
ただし、Ruby では特異メソッドやインスタンス変数を付け加えることができるので、厳密な意味で immutable とは言えません。ここでの判断基準は現在のインタプリタの挙動によります。 ↩
もうひとつちなむと、この 2 命令の YARV 命令列は何も無いメソッドをコンパイルしても生成されます。 ↩
異なるオブジェクト、というのはオブジェクトID(obj.object_id で取れる値)が異なるオブジェクト、という意味です。immutable なオブジェクトでは、プログラムの同じ場所では、毎回同じオブジェクトID を持つオブジェクトが生成されますが、文字列オブジェクトはそうではありません。簡単な実験としては、 ↩