2022-04-08

[php-src読書録]その8: forループ

php-srcのコードリーディングした内容をコツコツ残すテスト。その8

今日はforループ

<?php

for ($i = 0; $i < 2; $i++) {
  echo $i;
}

opcodes

$_main:
     ; (lines=7, args=0, vars=1, tmps=3)
     ; (before optimizer)
     ; /path/to/8.php:1-6
     ; return  [] RANGE[0..0]
0000 ASSIGN CV0($i) int(0)
0001 JMP 0004
0002 ECHO CV0($i)
0003 PRE_INC CV0($i)
0004 T3 = IS_SMALLER CV0($i) int(2)
0005 JMPNZ T3 0002
0006 RETURN int(1)

AST

zend_compile_for() でforがコンパイルされる。

zend_compile_expr_list()$i = 0; の部分がコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5258:L5258

zend_emit_jump() でJMPのopcodeが生成される。ジャンプ先は後で設定する。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5261
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2138-L2144

opnum_jmpはopcodesのindex

zend_compile_stmt() でループ内のstmtのコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5266

zend_compile_expr_list() でループの加算処理 $i++ をコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5269

zend_update_jump_target_to_next() で最初のJMPオペコード opnum_jmp のジャンプ先を設定している
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5272
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2204

zend_compile_expr_list() で条件 $i < 2 をコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5273

zend_emit_cond_jump() で条件を満たした場合にstmtの最初に飛ぶようにJMPNZを入れる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5276

pass_two() でゴニョる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1074-L1076

JMPやJMPNZに関してはopline_numにジャンプ先にインデックスがセットされているので、ジャンプ先〜ジャンプ元の差分を入れている

((char*)&(op_array)->opcodes[opline_num] - (char*)(opline))

opcode

ZEND_JMP_SPEC_HANDLER ではoplineを加算してジャンプする。

ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op1), 0);

今回はopline->op1 には 96=3 opcode分が入っていて、次はIS_SMALLERのopcodeが動く

ZEND_IS_SMALLER_SPEC_TMPVARCV_CONST_JMPNZ_HANDLER では条件に合致する場合 ZEND_VM_SMART_BRANCH_TRUE_JMPNZ() が呼ばれる。

中身はこれ↓

ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline + 1, (opline+1)->op2));

次のoplineからoplineのop2の分ジャンプすることになる。今回はJMPNZからop2.jmp_offset = -96 = 3 opcode分戻ることになり、最初のechoのopコードに到達する。

条件に合致しない場合は ZEND_VM_SMART_BRANCH_FALSE_JMPNZ() が呼ばれて中身はこれで、2 opcode分進んでループを抜ける。

ZEND_VM_SET_NEXT_OPCODE(opline + 2)

break/continue

breakはこのあたりでコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5077-L5079

CG(context).current_brk_contzend_begin_loop() で作られたforのスタック

zend_begin_loop() で brk_cont_element というスタックを作っている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L668-L670

zend_end_loop() で作ったスタックにcontinueとbreakのアドレスを入れる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L695-L696

continueは opnum_loop の値=ループ条件のopcodesのindex、 breakはループ終わりの次の命令(今回はJMPNZの次の命令)のopcodesのindexを示す。

例のごとく、これらは pass_two() でゴニョられる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1057-L1065

zend_get_brk_cont_target() でforループの brk_cont_array に入れたbrk, contのindexがopline_numにセットされる。 https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L658-L670

さらにZEND_JMP に変換され、opline_numは ZEND_PASS_TWO_UPDATE_JMP_TARGET() によって相対パスになる。 (BREAKやCONTINUEといったopcodeは存在せず JMP を使って実現している)

インクリメント

↓のオペコードもついでに調べた

PRE_INC CV0($i)

zend_compile_post_incdec() でコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8827-L8830

え、post_incdecなのにpre_inc?ってなって調べてみたら zend_do_free() がやってた
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L719-L728

resultがどこにも使われていない場合はPREになって未使用フラグが立つっぽい

ZEND_PRE_INC_SPEC_CV_RETVAL_UNUSED_HANDLER がハンドラー。 fast_long_increment_function() を呼び出してインラインアセンブラで対応。

	__asm__ goto(
		"addq $1,(%0)\n\t"
		"jo  %l1\n"
		:
		: "r"(&op1->value)
		: "cc", "memory"
		: overflow);
	return;
overflow: ZEND_ATTRIBUTE_COLD_LABEL
	ZVAL_DOUBLE(op1, (double)ZEND_LONG_MAX + 1.0);
このエントリーをはてなブックマークに追加