はじめに

ここではYARVコードへのコンパイルで生成したYARVコードを実行する処理を読解したいと思います。

rb_iseq_eval(vm.c)

YARVコード実行のエントリーポイントとなるのはrb_iseq_eval関数ですがこの関数はrb_vm_set_stack_top関数を呼んだ上でvm_eval_body関数を呼んでいるだけです。

vm_set_stack_top(vm.c)

vm_set_stack_top関数ではまずrb_vm_set_finish_env関数を呼んでフレームを生成しています。どうやらこのフレームはfinish命令を実行してYARVを終了するためのものなようです。

次にvm_push_frame関数を呼んで実行するYARVコードの情報をフレームに積んでいます。vm_push_frame関数は初期化のときもちらっと見ましたが再掲します。

    vm_push_frame(th, iseq, FRAME_MAGIC_TOP,
		  th->top_self, 0, iseq->iseq_encoded,
		  th->cfp->sp, 0, iseq->local_size);
vm_push_frame(rb_thread_t *th, rb_iseq_t *iseq, VALUE type,
	      VALUE self, VALUE specval, VALUE *pc,
	      VALUE *sp, VALUE *lfp, int local_size)
{
    ...
    /* nil initialize */
    for (i=0; i < local_size; i++) {
	*sp = Qnil;
	sp++;
    }

    /* set special val */
    *sp = GC_GUARDED_PTR(specval);
    dfp = sp;

    if (lfp == 0) {
	lfp = sp;
    }

    cfp = th->cfp = th->cfp - 1;
    cfp->pc = pc;
    cfp->sp = sp + 1;
    cfp->bp = sp + 1;
    cfp->iseq = iseq;
    cfp->flag = type;
    cfp->self = self;
    cfp->lfp = lfp;
    cfp->dfp = dfp;
    cfp->proc = 0;

例のmontecarlo.rbのiseqをフレームを積んだ後のスタックは以下のような感じになります。

         |Qnil
         |GC_GUARDED_PTR(0)
         |Qnil
         |GC_GUARDED_PTR(GC_GUARDED_PTR(0))
         |Qnil # for n
         |Qnil # for pi
         |Qnil # for svar
dfp,lfp→|GC_GUARDED_PTR(0)
  sp,bp→|
         |
         ...
         |
    cfp→|今積んだフレーム情報
         |rb_vm_set_finish_env関数で積んだフレーム情報
         |th_init2関数で積んだフレーム情報

vm_eval_body(vm.c)

この関数はvm_eval関数を実行します。例外やbreakなどが起こるとこの関数に戻ってきて適切な再開アドレスを計算し、再びvm_eval関数を呼び出しています。

vm_eval(vm_evalbody.c)

この関数がYARV命令実行の肝です。いい感じに難解なコードになっています。YARV命令はコンパイルオプションにより以下のいずれかの形式で実行されます。

OPT_CALL_THREADED_CODE
関数呼び出し
OPT_DIRECT_THREADED_CODE
goto
指定なし
switch文

詳しいからくりはvm.hを眺めてください。

vm.inc

各命令の処理ルーチンはどこに書かれているかですがvm.incに書かれています。

insns.def

各命令の動きが知りたい場合はinsns.defを見ると書いてあります。見るとわかりますがinsns.defに書かれているのはCではありません。tool/insns2vm.rbを実行することで各種incファイルが生成されるようです。各命令は以下のフォーマットになっているようです。

DEFINE_INSN
命令名
(引数...)
(スタックから拾う値)
(スタックに積む値)
{
    Cのソース
}

引数の取得やスタックからのポップ、スタックへのプッシュは定義から自動生成されるので命令を書く際に気にする必要はありません。ただし、任意個の要素をスタックからポップ、スタックへプッシュする際は自分で書く必要があります。

命令が実行される環境

YARVで使われるレジスタ

YARVでは以下の6つのレジスタが使われています。

cfp
現在の実行環境を表すrb_control_frame_t構造体(vm_core.h)へのポインタです。以下の5つのレジスタはrb_control_frame_tのメンバーです
dfp
ローカル変数領域へのVALUEポインタです
lfp
ローカル変数領域へのVALUEポインタです。dfpとの違いはブロック呼び出しでは変わらない点です。詳しいことはブロック呼び出しを見るときに説明します
sp
現在のスタックトップを示すVALUEポインタです
bp
現在のフレームが積まれた時のspの初期位置を示すVALUEポインタです
pc
現在実行している命令を示すVALUEポインタです

命令サポートマクロ

命令を記述する際に利用するマクロはinsnhelper.hに書かれています。スタック操作、ローカル変数操作、レジスタ操作などのマクロが定義されています。

命令サポート関数

命令を記述するする際に利用する関数はvm_insnhelper.cに書かれています。staticですがvm.cがincludeしているので命令内では問題なく使用することができます(vm_eval関数が書かれているvm_evalbody.cもvm.cがincludeしています)。

実行してみる

それではYARVコードへのコンパイルでコンパイルしたコードを実行してみます。

defineclass

この命令は名が示すようにクラスを定義します。まあそれ自体は珍しくないので珍しい部分を取り上げます。この命令ではスタックからベースクラス(スーパークラスではありません。Outer::Innerとなってる場合のOuterのことです)とスーパークラスを取得します。このうちベースクラスは以下の場合nilです。

  • トップレベル
    class C
    end
  • ネストされてるけど以下みたいな場合
    class Outer
      class Inner
      end
    end

vm_get_cbase(vm.c)

ベースクラスがnilの場合、vm_get_cbase関数が呼ばれてベースクラスを取得しています。 まず、フレームをサーチ(vm_get_ruby_level_cfp関数(eval_intern.h))してRubyで書かれたメソッドを実行しているフレームを取得しています。命令の処理ルーチンから呼ばれるので現在のフレームになると思います。 次にフレームのrb_iseq_t構造体からcref_stackという情報を取り出しています(get_cref関数(vm_insnhelper.c))。 その後、cref_stackからベースクラスを取得しています。

というのが処理の流れなのですがcref_stackっていつ設定されたのかというとYARVコードへのコンパイルの際(set_relation関数(iseq.c))に設定されています。

    if (type == ISEQ_TYPE_TOP) {
	/* toplevel is private */
	iseq->cref_stack = NEW_BLOCK(th->top_wrapper ? th->top_wrapper : rb_cObject);
	iseq->cref_stack->nd_file = 0;
	iseq->cref_stack->nd_visi = NOEX_PRIVATE;
    }
    else if (type == ISEQ_TYPE_METHOD || type == ISEQ_TYPE_CLASS) {
	iseq->cref_stack = NEW_BLOCK(0); /* place holder */
	iseq->cref_stack->nd_file = 0;
    }

というわけでトップレベルのベースクラスは普通Objectです(ベースクラスがObjectの場合はObject::CではなくCという名前になります)。 一方、クラス定義をコンパイルするときは0です。それじゃあ困るんじゃないか?といった疑問を解決するために次に行きます。

vm_cref_push(vm.c)

クラスを定義するとクラスを表すVALUEが決定されます。先ほどcref_stackのnd_clssが0だったのはこのクラスを表すVALUEが決定できないためです。この関数を呼ぶことでcref_stackにクラスが設定され、Innerをdefineclassする際にベースクラスとしてOuterを取得することができます。

setinlinecache, getinlinecache

この命令の組はVALUEをキャッシュに設定、キャッシュから取得するためのもので定数の値をキャッシュするなどに使われています。主な目的はクラスを示すVALUEをキャッシュして毎回テーブルを引かなくて済むようにすることだと思います。コンパイル時にOPT_INLINE_CONST_CACHEが有効の場合、定数取得のために出力されるYARVコードは以下のようになります。

:label(5)
getinlinecache(0, label(6))
getconstant(:MonteCarlo)
setinlinecache(label(5))
:label(6)

上の疑似コードはNODEがどういうYARV命令に変換されるかを示したものなのでgetinlinecacheの第1引数が0になってますが、VALUEコードとして設定される際にちゃんと領域が確保されます。

compile.c

VALUE v = (VALUE)NEW_INLINE_CACHE_ENTRY();
generated_iseq[pos + 1 + j] = v;

vm_core.h

#define NEW_INLINE_CACHE_ENTRY() NEW_WHILE(Qundef, 0, 0)
#define ic_class  u1.value
#define ic_method u2.node
#define ic_value  u2.value
#define ic_vmstat u3.cnt
typedef NODE *IC;

getinlinecacheの処理ルーチンは以下のようになっています。

DEFINE_INSN
getinlinecache
(IC ic, OFFSET dst)
()
(VALUE val)
{
    if (ic->ic_vmstat == GET_VM_STATE_VERSION()) {
	val = ic->ic_value;
	JUMP(dst);
    }
    else {
	 /* none */
	 val = Qnil;
    }
}

次にsetinlinecacheの処理ルーチンです。

DEFINE_INSN
setinlinecache
(OFFSET dst)
(VALUE val)
(VALUE val)
{
    IC ic = GET_CONST_INLINE_CACHE(dst);

    ic->ic_value = val;
    ic->ic_vmstat = GET_VM_STATE_VERSION();
}

insnhelper.h

#define GET_CONST_INLINE_CACHE(dst) ((IC) * (GET_PC() + (dst) + 1))
  若干わかりにくいので解説しておくと、getinlinecacheの第1引数を取得しています

定数取得の場合、setinlinecacheの前にgetconstantがあるのでスタックトップには定数名に対応したVALUEが積まれています。それを拾ってキャッシュに保存後、またスタックに積んでおくということを行っています。

後はGET_VM_STATE_VERSIONマクロが何者かわかれば理解できそうです。

vm.h

#define GET_VM_STATE_VERSION() (ruby_vm_global_state_version)
#define INC_VM_STATE_VERSION() \
  (ruby_vm_global_state_version = (ruby_vm_global_state_version+1) & 0x8fffffff)

vm.c

rb_vm_change_state(void)
{
    INC_VM_STATE_VERSION();
}

variable.c

mod_av_set(VALUE klass, ID id, VALUE val, int isconst)
{
    ...
    if(isconst){
	rb_vm_change_state();
    }

rb_const_set(VALUE klass, ID id, VALUE val)
{
    ...
    mod_av_set(klass, id, val, Qtrue);
}

というわけで、定数が設定されるとバージョンが上がっています。ご存じのようにRubyでは定数と言っておきながら何回でも代入できるので代入されたらキャッシュは無効になるというからくりのようです*1

send

この命令はメソッドを呼び出します。おそらくもっともよく実行される命令でしょう。

レシーバはFCALL_BITが設定されているとself、それ以外はTOPN(num)です。TOPN(num)が何故レシーバかというとスタックにはこう積まれているからです。

TOPN(2)→|レシーバ
TOPN(1)→|引数1
TOPN(0)→|引数2
    sp→|

その後、CALL_METHODマクロ経由でvm_call_method関数が呼び出され、メソッドの種類(Rubyで書かれてるかCで書かれているかなど)に応じて処理が行われています。Rubyで書かれたメソッドの場合、vm_setup_method関数に処理が移っています。

vm_setup_method(vm_insnhelper.c)

vm_setup_method関数ではまずvm_callee_setup_arg関数を呼び出してデフォルト引数や可変長引数の処理を行っています。いろいろ興味深いのですが例題の都合で実際の値でトレースすることができないので深追いは止めます。

その後、leaveの前のsendかによって分岐してますが、vm_push_frame関数を呼んでメソッド呼び出し完了です。・・・ちょっとだまされたような感じです。何でメソッド呼び出し完了なのかというと

  1. vm_push_frame関数を呼ぶことでth->cfpは新しいフレームを指すようになる
  2. vm_call_methodはQundefを返す
  3. CALL_METHODマクロを見るとQundefが返ってくるとRESTORE_REGSマクロが呼ばれている
  4. RESTORE_REGSマクロによりvm_eval関数のreg_cfpがth->cfpに更新される
  5. NEXT_INSNマクロによりreg_cfp->pcが指す命令が実行される

sendを呼び出す前はこうです。

reg_cfp->pc→send(:pi, 1, 0, 0, 0)
             setlocal(1) # pi

sendを呼び出した後はこうです。

reg_cfp->pc→putobject(0)

呼び出したとは書きましたが返ってきたとは書いてません:-)

send(ブロックを渡す場合)

メソッドにブロックを渡す場合を考えましょう。まずcaller_setup_arguments関数にてrb_block_t構造体が設定されています。

vm_insnhelper.c

caller_setup_args(rb_thread_t *th, rb_control_frame_t *cfp, VALUE flag,
		  int argc, rb_iseq_t *blockiseq, rb_block_t **block)
{
    rb_block_t *blockptr = 0;

    if (block) {
	if (flag & VM_CALL_ARGS_BLOCKARG_BIT) {
	    ...
	else if (blockiseq) {
	    blockptr = RUBY_VM_GET_BLOCK_PTR_IN_CFP(cfp);
	    blockptr->iseq = blockiseq;
	    blockptr->proc = 0;
	    *block = blockptr;
	}

RUBY_VM_GET_BLOCK_PTR_IN_CFPマクロはvm_core.hに書かれていますが若干不可解です。

#define RUBY_VM_GET_BLOCK_PTR_IN_CFP(cfp) ((rb_block_t *)(&(cfp)->self))

何故これでrb_control_frame_tからrb_block_tが取れるかというとこういうことです。

rb_control_trb_block_t
VALUE*pc
VALUE*sp
VALUE*bp
rb_iseq_t*iseq
VALUEflag
VALUEselfself
VALUE*lfplfp
VALUE*dfpdfp
rb_iseq_t*block_iseqiseq
VALUEprocproc
IDmethod_id
VALUEmethod_class
VALUEprof_time_self
VALUEprof_time_chld

つまり、selfから5個の型が同じなのでキャストによりrb_block_tになるわけです。

vm_yield(vm.c)

次にブロックが呼び出される部分を見てみましょう。Cでメソッドを書く場合rb_yield関数(eval.c)を呼び出します。その後、rb_yield_0関数経由でvm_yield関数が呼ばれます。

vm_yield関数ではlfpが指すアドレスからrb_block_t構造体を取得しています。念のためメソッド呼び出しの部分を見てみると、

vm_setup_method(rb_thread_t *th, rb_control_frame_t *cfp,
		int argc, rb_block_t *blockptr, VALUE flag,
		VALUE iseqval, VALUE recv, VALUE klass)
{
    ...
	vm_push_frame(th, iseq,
		      FRAME_MAGIC_METHOD, recv, (VALUE) blockptr,
		      iseq->iseq_encoded + opt_pc, sp, 0, 0);

となっています。その後、invoke_block関数にてフレームを積んでいます。

	vm_push_frame(th, iseq, type,
		      self, GC_GUARDED_PTR(block->dfp),
		      iseq->iseq_encoded + opt_pc, cfp->sp + arg_size, block->lfp,
		      iseq->local_size - arg_size);

スタックがどうなるか見てみましょう。サンプルは例によってmontecarlo.rbです。

       |Qnil # for n
       |Qnil # for count
       |Qnil # for svar
  lfp→|GC_GUARDED_PTR(0)
       |Qnil # for x
       |Qnil # for y
  dfp→|GC_GUARDED_PTR(block->dfp)
sp,bp→|

今まで見てきたのと異なり、lfpとdfpが同じではありません。

opt_plus

コンパイル時に最適化オプションが設定されていると、send(+)命令がこの命令に置き換えられます。処理ルーチンは以下のようになっています(#ifとかは除去。後、#ifndef LONG_LONG_VALUEもカットしてます)。

insns.def

DEFINE_INSN
opt_plus
()
(VALUE recv, VALUE obj)
(VALUE val)
{
    if (FIXNUM_2_P(recv, obj) &&
	BASIC_OP_UNREDEFINED_P(BOP_PLUS)) {
	/* fixnum + fixnum */
	long a, b, c;
	a = FIX2LONG(recv);
	b = FIX2LONG(obj);
	c = a + b;
	if (FIXABLE(c)) {
	    val = LONG2FIX(c);
	}
	else {
	    val = rb_big_plus(rb_int2big(a), rb_int2big(b));
	}
    }
    else if (HEAP_CLASS_OF(recv) == rb_cFloat &&
	HEAP_CLASS_OF(obj) == rb_cFloat &&
	BASIC_OP_UNREDEFINED_P(BOP_PLUS)) {
	val = DOUBLE2NUM(RFLOAT_VALUE(recv) + RFLOAT_VALUE(obj));
    }
    else if (HEAP_CLASS_OF(recv) == rb_cString &&
	HEAP_CLASS_OF(obj) == rb_cString &&
	BASIC_OP_UNREDEFINED_P(BOP_PLUS)) {
	val = rb_str_plus(recv, obj);
    }
    else if (HEAP_CLASS_OF(recv) == rb_cArray &&
	BASIC_OP_UNREDEFINED_P(BOP_PLUS)) {
	val = rb_ary_plus(recv, obj);
    }
    else {
	PUSH(recv);
	PUSH(obj);
	CALL_SIMPLE_METHOD(1, idPLUS, recv);
    }
}

何をしているかというと、

  1. 特定のクラスのオブジェクトで
  2. +が再定義されていなかったら
  3. 直接計算もしくはメソッドの実装関数を直接呼び出すことでフレームに積むなどの処理をバイパス

ということをしています。BASIC_OP_UNREDEFINED_Pマクロは以下のようになっています。

insnhelper.h

#define BASIC_OP_UNREDEFINED_P(op) ((ruby_vm_redefined_flag & (op)) == 0)

というわけで再定義チェックもloadとandとtest(i386以外でどういうのかわからない)ぐらいのマシン語で済むのでチェックのコストもそんなにかかりません。

ruby_vm_redefined_flag(vm.c)

次にruby_vm_redefined_flagを変更している周りを眺めてみましょう。まず初期化。

Init_VM(void)
{
    ...
    vm_init_redefined_flag();
}
vm_init_redefined_flag(void)
{
    ID mid;
    VALUE bop;

    vm_opt_method_table = st_init_numtable();

#define OP(mid_, bop_) (mid = id##mid_, bop = BOP_##bop_)
#define C(k) add_opt_method(rb_c##k, mid, bop)
    OP(PLUS, PLUS), (C(Fixnum), C(Float), C(String), C(Array));

展開するとこんな感じになります。

mid = idPLUS, bop = BOP_PLUS,
(add_opt_method(rb_cFixnum, mid, bop),
 add_opt_method(rb_cFloat, mid, bop),
 add_opt_method(rb_cString, mid, bop),
 add_opt_method(rb_cArray, mid, bop));

で、add_opt_method関数。

add_opt_method(VALUE klass, ID mid, VALUE bop)
{
    NODE *node;
    if (st_lookup(RCLASS_M_TBL(klass), mid, (void *)&node) &&
	nd_type(node->nd_body->nd_body) == NODE_CFUNC) {
	st_insert(vm_opt_method_table, (st_data_t)node, (st_data_t)bop);
    }
}

というわけでメソッド実装関数のNODEとBOPが関連づけられています。

次にメソッド定義時の動作です。Rubyで再定義したとしてvm_define_method関数をスタート地点にします。

vm_insnhelper.c

vm_define_method(rb_thread_t *th, VALUE obj,
		   ID id, rb_iseq_t *miseq, rb_num_t is_singleton, NODE *cref)
{
    ...
    rb_add_method(klass, id, newbody, noex);

eval_method.c

rb_add_method(VALUE klass, ID mid, NODE * node, int noex)
{
	...
	/* check re-definition */
	st_data_t data;
	NODE *old_node;

	if (st_lookup(RCLASS_M_TBL(klass), mid, &data)) {
	    old_node = (NODE *)data;
	    if (old_node) {
		if (nd_type(old_node->nd_body->nd_body) == NODE_CFUNC) {
		    rb_vm_check_redefinition_opt_method(old_node);
		}

vm.c

rb_vm_check_redefinition_opt_method(NODE *node)
{
    VALUE bop;

    if (st_lookup(vm_opt_method_table, (st_data_t)node, &bop)) {
	ruby_vm_redefined_flag |= bop;
    }
}

といった感じにruby_vm_redefined_flagが変更されています。とするとString#+が変更されるとFixnumの最適化も働かなくなるのですが、+が再定義されることはまずないからいいだろうということだと思われます。それに再定義チェックを厳密にしていたらバイパスしている意味がなくなりますし:-)

おわりに

今回はYARVコードへの実行を見てきました。YARVコードまで落ちると個々の命令は単純なので実行モデルが理解できれば他の命令も理解できると思います。

一応ここまででrubyコマンドを打ってからスクリプトが実行されるまでの流れは終了です。ただしたっぷり未解決のものがあるのでそれらは個別に書きたいと思います。それではみなさんもよいコードリーディングを。


*1 実際には他にもバージョンを増やしているものがあるので代入以外でも無効になります

トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2008-02-26 (火) 22:22:06 (5896d)