ワナビーエンジニアのブログ

なんでもいいから文化的な生活を送りたい

Rubyのメソッド呼び出しの流れ(1)

Rubyの#13053を読んでみる(1)から見ていった中で、 rubyArray#select!を書いた後に、どうのようにrb_ary_select_bangが呼ばれているのか気になったので見ていきます。 (現在 svnのrevisionは57251)

以下、RubyのしくみとYarvManiacsを参考に、内容を咀嚼していく物になります。

既存の情報をググる

まずググって出てきたるびまを読みました。 YARV作者のささださんが書かれたYarvManiacsという連載が、私の知りたいこととドンピシャでハマっているようなので大変参考になりました。

Rubyist Magazine - YARV Maniacs 【第 6 回】 YARV 命令セット (3) メソッドディスパッチ

この例では、Array#lengthを追っていて、メソッド呼び出しはYARVではsend命令らしい。オペランドは5つ。

最新環境(Ruby2.4)でやってみる

最新のrubyでもやってみます。

Array#length

るびまの例をそのままYARVを出力。

code = <<END
[1,2].length
END

puts RubyVM::InstructionSequence.compile(code).disasm

出力

== disasm: #<ISeq:<compiled>@<compiled>>================================
0000 trace            1                                               (   1)
0002 duparray         [1, 2]
0004 opt_length       <callinfo!mid:length, argc:0, ARGS_SIMPLE>, <callcache>
0007 leave

なるほど?send命令じゃないです。 opt_lengthという専用の命令に置き換えられているっぽい。 高速化の為でしょうか?オペランドも2つしかありません。 これはYarvManiacsの連載第9回に答えがありました。

Rubyist Magazine - YARV Maniacs 【第 9 回】 特化命令

特価命令っていうみたいで、 るびまの第6回の時代のrubyとは、処理系が変わっていますね。 ということは、当然今日までも結構変わっていそうですね・。・;

Array#select!

テスト用に用いたコード。#13053のままですが、簡略化のためにpを削っています。

code = <<END
ary = [1,2,3,4,5]
ary.select! { |i| ary.clear if i==5; false }
# p ary
# p ary.size
END

puts RubyVM::InstructionSequence.compile(code).disasm

結果↓

== disasm: #<ISeq:<compiled>@<compiled>>================================
== catch table
| catch type: break  st: 0008 ed: 0014 sp: 0000 cont: 0014
|------------------------------------------------------------------------
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] ary
0000 trace            1                                               (   1)
0002 duparray         [1, 2, 3, 4, 5]
0004 setlocal_OP__WC__0 3
0006 trace            1                                               (   2)
0008 getlocal_OP__WC__0 3
0010 send             <callinfo!mid:select!, argc:0>, <callcache>, block in <compiled>
0014 leave
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================
== catch table
| catch type: redo   st: 0002 ed: 0021 sp: 0000 cont: 0002
| catch type: next   st: 0002 ed: 0021 sp: 0000 cont: 0021
|------------------------------------------------------------------------
local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] i<Arg>
0000 trace            256                                             (   2)
0002 trace            1
0004 getlocal_OP__WC__0 3
0006 putobject        5
0008 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0011 branchunless     19
0013 getlocal_OP__WC__1 3
0015 opt_send_without_block <callinfo!mid:clear, argc:0, ARGS_SIMPLE>, <callcache>
0018 pop
0019 putobject        false
0021 trace            512
0023 leave

長々と張り付けておりますが、メソッド呼び出しは

0008 getlocal_OP__WC__0 3
0010 send             <callinfo!mid:select!, argc:0>, <callcache>, block in <compiled>

のsend命令です。その上がレシーバ。send命令のオペランドは3つ。 YARVのsend命令が:select!のメソッド(のシンボル)を与えている部分っぽいです。 るびまYarvManiact第6回の'ブロック付メソッド呼び出し'が参考になりました。 argc:0から引数は0。だけど、ブロックとしてblock in <compild>部分で<ISeq:block in <compiled>@<compiled>命令列を渡しているみたいです。 メソッドにブロックを渡す物は、単に引数を渡す処理方法と比べて特殊ということが見て取れました。

YARVのsend命令の実装

YARVが上記send命令での:select!をどう実行しているか追います。 sendはinsns.defで実装されています。

952/**
953  @c method/iterator
954  @e invoke method.
955  @j メソッド呼び出しを行う。ci に必要な情報が格納されている。
956 */
957DEFINE_INSN
958send
959(CALL_INFO ci, CALL_CACHE cc, ISEQ blockiseq)
960(...)
961(VALUE val) // inc += - (int)(ci->orig_argc + ((ci->flag & VM_CALL_ARGS_BLOCKARG) ? 1 : 0));
962{
963    struct rb_calling_info calling;
964
965    vm_caller_setup_arg_block(th, reg_cfp, &calling, ci, blockiseq, FALSE);
966    vm_search_method(ci, cc, calling.recv = TOPN(calling.argc = ci->orig_argc));
967    CALL_METHOD(&calling, ci, cc);
968}

C言語チックですが、引数の実装が良くわからない感じになっています。(ここ後日追いたい)。 959行目で引数が3つですが、、これはsend命令のオペランドの数と合致していることを表現しているみたいです。 ciに呼び出したいメソッドの情報が入っています。

この処理のざっくり解釈としては、

  • vm_caller_setup_arg_block(): vm_args.cに定義されている。ブロックを渡された場合は、calling->block_handlerにセットする関数みたい(いつかちゃんと読む)
  • vm_search_method(): vm_insnhelper.cに定義されている。コールキャッシュに乗っていれば即返し、なければrb_call_cache構造対に色々セットしている。
    • 1230 cc->me = rb_callable_method_entry(klass, ci->mid);: me(method entry)を探索している
    • 1232 cc->call = vm_call_general;: ナニコレ?
  • CALL_METHODマクロ: メソッド呼び出しの実施

です。ところでいちいちインデントが不揃いで読みづらいところが気になります。。

CALL_METHODマクロ

メソッド実施の処理マクロですが、キモっぽいので読んでいきます。

124#define CALL_METHOD(calling, ci, cc) do { \
125    VALUE v = (*(cc)->call)(th, GET_CFP(), (calling), (ci), (cc)); \
126    if (v == Qundef) { \
127    RESTORE_REGS(); \
128    NEXT_INSN(); \
129    } \
130    else { \
131    val = v; \
132    } \
133} while (0)

do{ }while(0)はマクロでブロックっぽく書くための小技なので無視。 125行目の(*(cc)->call)が目的の関数への関数ポインタみたいです。 cc(rb_call_cache構造体)の構造を知っておく必要がありそうですね。 他のRESTORE_REGSNEXT_INSNマクロは、次の命令に進めるマクロのようです。

つづきは次回

スポンサードリンク

Rubyのしくみ -Ruby Under a Microscope-

Rubyのしくみ -Ruby Under a Microscope-