YARV Maniacs 【第 5 回】 命令セット (2) リテラル・変数・定数

書いた人:ささだ

はじめに

YARV: Yet Another RubyVM の中を解説するこの連載。今回は前号の続きで YARV の命令セットを紹介します。

前号では、Ruby プログラムがどのような YARV の命令列に変換されたかを、とても簡単な例で示しました。今回も、簡単な例を続けます。

その前に - YARV 0.3.2

YARV をスレッドの実行に対応しました。つまり、 Thread.new{ 何か } と書くと別スレッドで「何か」が動きます。といっても、まだまだ不完全ですが。とくに I/O やシグナルまわり。そのあたりを触らないように、いじってもらえると幸いです。

現在の Ruby のスレッドは OS(など)が提供するネイティブスレッドを使わず、すべてユーザレベルで実現されています(俗にいうグリーンスレッドモデル)。そこで、YARV ではネイティブスレッドを使うようにしました。現在は pthread と Windows のスレッド環境に対応しています。

このあたりの話はいろいろあるのですが(何を隠そう、私の大学の卒論テーマはスレッドライブラリの開発です)、小難しくなるのでやめ。また、ネイティブスレッドを利用するにしても、どんな設計にするのか、選択肢とトレードオフがいろいろあります。そのあたりの話は [yarv-dev:631] Re: thread support から関連するスレッドに(試行錯誤の後として)まとまっています。興味のある方はこちらをどうぞ。

最適化された VM について

現在の YARV には、処理速度向上のためにいろいろな最適化の仕組みを実装しているのですが、それらはデフォルトではほとんど無効になっています。有効にして処理速度向上を実感したい方は、vm_opts.h というファイルを変更して有効にしてください。変更の仕方は、多分読めばわかると思います。今後は configure などでこの最適化の有無などを選択できるようにしたいと考えています。

デフォルトで最適化が無効なのは、最適化を有効にするとコンパイル速度がかなり遅くなってしまうというのが理由のひとつ。もうひとつの理由はこの連載で示している YARV 命令列と、最適化後に出力される(前回紹介した make parse で出力される)命令列が違うものになってしまうからです。

YARV - Compile and Disassemble CGI

Ruby プログラムを YARV 命令列に変換して、それを逆アセンブルして表示する CGI を作りました(YARV - Compile and Disassemble CGI)。

この CGI を見てもらうと、おっきなテキストフィールドがあると思うので、そこに適当な Ruby プログラムを書き込んで、「compile and disassemble」というボタンを押してみてください。ずらずらと色々な情報、つまりこの連載でちょうど説明している YARV 命令列が表示されます。

もちろん、コンパイルして逆アセンブルするだけなので、YARV でそのプログラムを実行するわけじゃありません。そんな恐ろしいことは出来ません1

いろいろ試して、Ruby プログラムがこんな命令に変換されるんだ、というのを見て楽しんでもらえればいいと思います。

ちなみに、もうひとつあるボタン「compile and disassemble (to optimized instructions)」は、コンパイル時に YARV 命令列を最適化して表示します。多分、見てもさっぱりわかんないんじゃないかと思います。この最適化された命令列の読み方は、いずれ紹介したいと思っています。

今回のテーマ:命令列 (2) リテラル・変数・定数

さて、前置きが長くなりましたが、今回は「リテラル・変数・定数」式を表現する YARV 命令列についての解説です。なぜ、このテーマを選んだかというと、とても簡単だからです。

具体的には、

1

とか、

/abc/

とか、

@a = 1

とか、そういうのを、YARV 命令列ではどうやって表現するのか解説します。見るからに簡単そうですね。

リテラル式

Ruby でリテラルというと、true/false/nil などの固定の値、数値(1、12345678901234567890 、1.2)、シンボル(:Symbol)、文字列(”String”)、正規表現(/abc/)などがあります。また、似たようなものに配列([1, 2, 3])、ハッシュ({1=> “a”, 2=> “b”})、範囲(1..3)などの式があります。

YARV 命令列では、これらのリテラル式はスタックにそのリテラルが示すオブジェクトを積む、という操作になります。

immutable なオブジェクト

変更できないオブジェクトを immutable なオブジェクトと言います2。たとえば、true オブジェクトはいつも true です。123 はいつも 123 です。内容が変わったりはしません。前述した例では、true/false/nil などの固定の値、数値(1、12345678901234567890 、1.2)、シンボル(:Symbol)、正規表現(/abc/)などがこれにあたります。

これらのリテラル式は putobject という命令で表現されます。

a = true
a = false
a = 123
a = :Symbol
a = /abc/
nil

という Ruby プログラムは次のようにコンパイルされます。

0000 putobject       true
0002 setlocal        2
0004 putobject       false
0006 setlocal        2
0008 putobject       123
0010 setlocal        2
0012 putobject       :Symbol
0014 setlocal        2
0016 putobject       /abc/
0018 setlocal        2
0020 putnil
0021 end

setlocal というのは、「ローカル変数 a (命令列中では 2 と表現されている)に、そのリテラルの値を代入する」、という意味です。これについては簡単そうで難しいので次回説明します。end は前号でも紹介したとおり、そのスコープの終わり(そして呼び出し元、例えばメソッドコールの返り値としてスタックトップの値を返す)、という意味ですが、これもあわせて次回に。

setlocal、end を除くと、上記命令列には「putobject [何か]」という命令と、putnil という命令だけになります。putobject は、[何か] をスタックに積む、という意味で、putnil は nil をスタックに積む、という意味になります。putobject nil と書いてもまったく同様なのですが、putobject nil のパターンは大量に出てきて、コンパイラを書くときに面倒くさかったので一個命令を追加しました(今思えば、マクロにすればよかった)。

これは簡単ですね。

余談ですが、

true
false
123
:sym
/abc/
nil

という何もしない Ruby プログラムをパースするとどうなるでしょう。

0000 putnil
0001 end

何もしない YARV 命令列が生成されました3

文字列リテラル

次は文字列リテラルです。文字列リテラルは破壊的な変更が可能なので mutable なオブジェクトと言えます。たとえば、プログラム上同じ位置にあっても、文字列リテラルが返すオブジェクトは毎回異なります4

というわけで、毎回同じオブジェクトをスタックトップに置くだけの putobject 命令は使えないので、代わりに putstring 命令を使います。

a = 'abc'
nil

というプログラムは、

0000 putstring       "abc"
0002 setlocal        2
0004 putnil
0005 end

とコンパイルされます。面倒なので、これからは setlocal 以下は載せないようにしますね。

とくに難しいことはありません。

配列式・ハッシュ式

配列やハッシュ式、範囲式で生成されるオブジェクトも mutable です。これらの式の特徴は、先ほどの例とは違って、他のオブジェクトを利用して作ることです。たとえば、配列オブジェクトを生成する [1, 2, 3] という式は、Fixnum のオブジェクト 1, 2, 3 を利用して作ります。

これらはどのようにするかというと……、例を見たほうが早いですね。

["a", "b", "c"]

をコンパイルすると、

0000 putstring        "a"
0002 putstring        "b"
0004 putstring        "c"
0006 newarray         3   # スタックトップ 3 つをとってきて
                          # ひとつの配列にする

という命令列になります。つまり、スタックに “a”、”b”、”c” と積んで、最後に newarray 命令でスタックトップの 3 つ、つまり今積んだ “a”、”b”、”c” を取ってきて配列にする、という意味です。

では、要素が String ではなくて Fixnum である配列を見てみましょう。

[1, 2, 3]

このような Ruby プログラムを YARV 命令列にすると、次のようになります。

0000 duparray        [1, 2, 3]

要素がすべてリテラル(putobject 命令で済むもの)だった場合、毎回各要素をスタックに積まなくても作るものが決まっているため、duparray 命令ひとつ(配列オブジェクトを dup してスタックトップに積む)で済むことになります。

ハッシュは配列の要素数を 2 倍(1 要素 key と value で 2 つ)にしただけです。newhash 命令になります。

{1 => "a", 2 => "b", 3 => "c"}
0000 putobject       1
0002 putstring       "a"
0004 putobject       2
0006 putstring       "b"
0008 putobject       3
0010 putstring       "c"
0012 newhash         6   # 3 要素なのでスタックトップから
                         # 6 オブジェクト取ってくる

範囲式

範囲式は Range オブジェクトを生成します。これを行うための命令 newrange は先端と終端を示すオブジェクト 2 つをスタックトップから取り出し、新しい Range オブジェクトを生成します。

[('a'..'b'), ('a'...'b')]

というプログラムは、

0000 putstring       "a"
0002 putstring       "b"
0004 newrange        0
0006 putstring       "a"
0008 putstring       "b"
0010 newrange        1
0012 newarray        2

このような YARV 命令列になります。(x..y) の場合は newrange の命令オペランドに 0 を、(x…y) の場合は 1 を指定してあります。

さて、ここで

(1..10)

という、よくありがちな Range 式をコンパイルしてみましょう。

0000 putobject       1..10

newrange 命令はありません。これは、Range オブジェクトは(多分) immutable なので、範囲を指定する式が二つとも immutable である場合はこのような最適化が可能なのです。

変数

ここでは変数について説明します。ただ、ローカル変数は面倒くさいので次号にまわします。

インスタンス変数

YARV 命令として、インスタンス変数を設定したり、値を取得するための特別な命令を用意しています。

@a = 1
a = @a

は次のような YARV 命令列になります。

0000 putobject            1
0002 setinstancevariable  :@a
0004 getinstancevariable  :@a

set/getinstancevariable という命令があるだけです。とても簡単。

グローバル変数

YARV 命令として、グローバル変数を設定したり、値を取得するための特別な命令を用意しています。

$global = 1
a = $global

は次のような YARV 命令列になります。

0000 putobject        1
0002 setglobal        $global
0004 getglobal        $global
0006 setlocal         2

set/getglobal という命令があるだけです。とても簡単。

クラス変数

YARV 命令として、クラス変数を設定したり、値を取得するための特別な命令を用意しています。

@@a = 1
a = @@a

は次のような YARV 命令列になります。

0000 putobject         1
0002 setclassvariable  :@@a, true
0005 getclassvariable  :@@a

set/getclassvariable という命令があるだけです。setclassvariable には true という命令オペランドが付いてますね。これはなんでしょうか[^5]。えーと、調べてみると警告を出すためにクラス変数定義の場所で true / false が変わったりするようなのですが、今 ruby 1.9 のソース(つまり、YARV のソース)を確認すると、この値を見てませんねぇ(汗) リファクタリング対象のようです。

定数

Ruby の定数は不思議なことに、他の言語でいう定数ではありません。リフレクション機能を使ったり、警告を無視したりすれば簡単に再定義が可能です。

C = 1
C = 2
#=> test.rb:2: warning: already initialized constant C
# 警告は出るが、再定義される。

また、実行時に後述する定数検索を行わなければならないため、定数のくせにアクセスコストは定数ではありません(検索コストは検索パスの大きさに比例します)。そのため、一度検索したらその結果をキャッシュしておく__インラインキャッシュ__も用意していますが、それも後述します。

定数アクセス命令

定数アクセスには次のように、いくつか種類があります。

C    # その実行コンテキストで探す C
::C  # トップレベルで定義してある C
C::D # C をその実行コンテキストで探し、そのクラス / モジュール
       から D を検索

そのため、ただ C としてアクセスしているのか、::C としてアクセスしているのかで、意味が全然違います。そこで、定数の値を得る(スタックに積む)命令 getconstant はスタックオペランドに定数検索の起点を置くことにしました。

起点が nil だったら
現在の実行コンテキストで検索
起点がクラス・モジュールオブジェクトだったら
そのクラス・モジュールオブジェクトを起点に検索
# C
putnil
getconstant  :C
# ::C
putobject Object
getconstant  :C # Object の中の C (つまりトップレベル)を探る
# C::D
putnil
getconstant  :C # 現在の実行コンテキストで C を探る
getconstant  :D # C の中の D を探る

定数を設定する setconstant 命令も同様に、スタックオペランドで設定する場所を決めます。

ちょっとあいまいな言葉が多かったりしますが、厳密に言い出すととても面倒なのでこの辺で。

定数検索

定数の検索は、処理系実装者から見るとかなり作るのが面倒な部分になっています。ふつう気がつかないんだけど、細かい仕様がたくさんあるんです。詳細は RHG を読んでください(第 6 章 変数と定数第 14 章 コンテキスト)。要は、コンパイル時には定数検索パスが決まらないのです。

この話題は、実行コンテキストの話も含めて、より細かい実装の段階で解説します。

定数キャッシュ

さて、定数は定数という名前なのですから、再定義できるとはいっても、めったにそんなことは起こりません。そこで、一度値を検索したらキャッシュすることにしました。それを実現するのが get/setinlinecache 命令です。

cache_start:                  # ラベル
  getinlinecache cache_end
   [... 何か式 ...]
   setinlinecache cache_start
cache_end:                    # ラベル

getinlinecahce 命令は、まず自分自身が値をキャッシュしているかどうか確認しています。もしキャッシュしており、その値が使えたら位置 cache_end(setinlinecache 命令の後ろ)へジャンプします。そうでなければ、まず nil を積んでから[^6] [… 何か式 …] の部分を実行します。setinlinecache 命令は、位置 cache_start で示される getinlinecache 命令のキャッシュ領域(命令オペランド部分)に、[… 何か式 …] の計算結果をキャッシュします。

キャッシュした値が正しいかどうかは、定数を定義をすると増加するカウンタ値(これは VM グローバルなカウンタ)とともにキャッシュしておき、現在のそのカウンタ値とキャッシュしたときのカウンタ値を比べることでその値が使えるかどうか確認できます。

この命令は Ruby プログラムの意味の表現とは直接関係ありませんが、性能には結構影響します。

さて、これを踏まえて、次に示す定数アクセスプログラムの YARV 命令列へのコンパイル結果を見てみましょう。

[C, ::C, C::D]

次のようになります。

0000 getinlinecache  <ic>, 7    # <ic> については後述
0003 getconstant     :C
0005 setinlinecache  0
0007 getinlinecache  <ic>, 17
0010 pop                        # pop については後述
0011 putobject       Object
0013 getconstant     :C
0015 setinlinecache  7
0017 getinlinecache  <ic>, 28
0020 pop
0021 putnil
0022 getconstant     :C
0024 getconstant     :D
0026 setinlinecache  17
0028 newarray        3
0030 end

pop 命令はスタックからひとつ値を捨てる命令です。getinlinecache の命令オペランド <ic> は値をキャッシュする領域を示しています。

ちょっと長いですが、意味はそれぞれ単純なのでよく見ればわかると思います。

おわりに

今回も YARV 命令セットの説明を行いました。リテラル・変数・定数という、Ruby でも簡単なところばかりだったので、退屈してしまったかもしれません。次回は、命令セットの続きとして、もうちょっと難しいところに踏み込んでみようと思います。お楽しみに。

著者について

ささだこういち。学生。

いつもこの時期になると英語に対する危機感を覚えるんだけど、すぐにのど元を過ぎてしまう。まぁいいか(いや、良くないよ!)。

YARV Maniacs 連載一覧


loop{ p “String”.object_id # mutable / 毎回オブジェクト ID が変わる p true.object_id # immutable / オブジェクト ID は不変 p :Symbol.object_id # immutable / オブジェクト ID は不変 } というプログラムを動かすとわかります。 [^5]: 本稿執筆中、筆者はマジで忘れていた。 [^6]: nil を積むのは、getconstant 命令の出現箇所の多くで次の命令が putnil 命令であることが多いため。

  1. バグが多すぎる、ということがばれるのが怖い、というわけじゃなくて、セキュリティの問題です。もちろん。 

  2. ただし、Ruby では特異メソッドやインスタンス変数を付け加えることができるので、厳密な意味で immutable とは言えません。ここでの判断基準は現在のインタプリタの挙動によります。 

  3. もうひとつちなむと、この 2 命令の YARV 命令列は何も無いメソッドをコンパイルしても生成されます。 

  4. 異なるオブジェクト、というのはオブジェクトID(obj.object_id で取れる値)が異なるオブジェクト、という意味です。immutable なオブジェクトでは、プログラムの同じ場所では、毎回同じオブジェクトID を持つオブジェクトが生成されますが、文字列オブジェクトはそうではありません。簡単な実験としては、