ひとり勉強会

ひとり楽しく勉強会

poped 引数

なぜ突然スタックマシンの話を始めたかというと、実は、iseq_compile_each の第四引数を理解するのに必要だったからなのです。

iseq_compile_each(..., int poped)

ざっと見た感じ、poped=0 で呼び出されている場合の方が多いようでした。そこで、特殊な方のpoped=1 で呼び出す、COMPILE_POPED を使っている場所を検索してみました。引っ掛かったのは

else if (iseq->type == ISEQ_TYPE_ENSURE) {
    set_exception_tbl(iseq);
    COMPILE_POPED(list_anchor, "ensure", node);
}

ensure節の中身をコンパイルするところと、

case NODE_WHILE:
case NODE_UNTIL:{
  ...
  ADD_LABEL(ret, redo_label);
  COMPILE_POPED(ret, "while body", node->nd_body);
  ADD_LABEL(ret, next_label);	/* next */

whileやuntilの本体をコンパイルするところです。(多重代入のコンパイルのとこでも見つかったのですが、むずかしかったので今のところ省略です。)

なんとなくわかってきました。while文全体の値はいつもnilと決まっているので、本体の評価結果は使われません。begin式全体の値は本体かrescue節かelse節のどれかの値と決まっていて、ensure節の値は必ず無視されることになっています。

要するに、式の値が要らない式をコンパイルする時のみ、poped=1(COMPILE_POPED)が使われているってことです。逆にiseq_compile_each側から見れば

  • poped=0 のとき、式の評価結果をスタックにpushして終わるようコンパイルする
  • poped=1 のとき、式の評価結果はスタックにpushせずに終わるようコンパイルする

となっています。常に poped=0 でコンパイルして、値がいらないところでは明示的に pop する、っていうコンパイルの仕方もありだと思うんですが、多分それは効率が悪い(結局使われない値をpushしてpopするのは無駄)なので、そもそもpushしないコンパイルができるように、popedというフラグが用意されているようです。

実例

実際にいくつかコンパイルしてみて確認します。

puts
-----------------------------
0000 putself
0001 send             :puts, 0, nil, 12, <ic>
0007 leave

"puts" を poped=0 でコンパイルすると putself; send という2命令になっています。

while gets do puts end
-----------------------------
0000 jump             10
0002 putself
0003 send             :puts, 0, nil, 12, <ic>
0009 pop
0010 putself
0011 send             :gets, 0, nil, 12, <ic>
0017 branchif         2
0019 putnil
0020 leave

whileの中で(popoed=1 で)コンパイルすると putself; send; pop という3命令になりました。結果をスタックに積んでない状態にするためにpop命令が増えています。

123
-----------------------------
0000 putobject        123
0002 leave

"123" を poped=0 でコンパイルすると、putobject 1命令になります。

while gets do 123 end
-----------------------------
0000 putself
0001 send             :gets, 0, nil, 12, <ic>
0007 branchif         0
0009 putnil
0010 leave

poped=1 でコンパイルすると、なんと0命令になります。putobject; pop のような無駄な命令列は出さないようになっているわけですね。

余談

思いっきり余談です。実験してたら

while true do 123 end

コンパイルが(実行が、ではなく)終わらないという事件が勃発しました。どうも、上で書いた余計なpushを出さない処理と条件式が定数のときの最適化か何かがあわさって

  jump next_label
redo_label:
  # while の本体(空)
next_label:
  # 終了条件判定(空)
  jump redo_label

こういうコードが一旦生成されるせいのようです。最後のジャンプ命令が自分自身にジャンプするので、「jump redo_label はジャンプ先がまたジャンプ命令なので、一気に二段分ジャンプする」最適化が無限ループしているみたい。
でも最新版ではどうだろう?と思って Subversionのtrunk を見たら、

else if (iobj != diobj && diobj->insn_id == BIN(jump)) {
    OPERAND_AT(iobj, 0) = OPERAND_AT(diobj, 0);
    goto again;
}

ばっちり修正されてました。やった!