for ループのオペコードで PHP5 と PHP7 の違いを見る
先日の PHP カンファレンス 2015 で、蒋池東龍さんによる「PHP あるあるパフォーマンス対決」という発表がありました。私は当日は他のセッションに参加していたのですが、処理系内部のこのような話には興味があり、後から YouTube で楽しませてもらいました。当日のツイートのまとめも togetter で公開されています。
PHP conference 2015 トラック3 - YouTube
君は「本当に速い」PHPを書いているか!? PHPあるある速度対決 #phpcon2015 #phpcon2015_3 - Togetterまとめ
この発表がモチベーションとなって PHP 7 ではどうなのだろうと調べてみたところ、単純な for ループから生成されるオペコードにも PHP 5 と PHP 7 で違いがあり面白かったので、紹介します。
次のような for ループを考えます。これがどのようなオペコードになるかを vld を利用して確認してみます。
<?php for ($i = 0; $i < 10; ++$i) { $x = 1; }
vld の作者のウェブページは以下にあります。GitHub から最新版を取得できます。PECL でも提供されていますが、現時点では、最新の 0.13.0 でも PHP 7.0.0 RC4 ではビルドできなかったので、GitHub から取得するのがよさそうです。
Projects — Derick Rethans
PECL :: Package :: vld
さて冒頭のコードについて、PHP 5.6.14 では、次のようなオペコードになります。
$ php -dvld.active=1 -dvld.execute=0 test.php
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 0
1 > IS_SMALLER ~1 !0, 10
2 > JMPZNZ 5 ~1, ->7
3 > PRE_INC !0
4 > JMP ->1
4 5 > ASSIGN !1, 1
5 6 > JMP ->3
6 7 > > RETURN 1
(命令列以外の出力内容は省略)一方、PHP 7.0.0 RC4 では、次のようになります。
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 0
1 > JMP ->4
4 2 > ASSIGN !1, 1
2 3 PRE_INC !0
4 > IS_SMALLER ~5 !0, 10
5 > JMPNZ ~5, ->2
6 > > RETURN 1
(命令列以外の出力内容は省略)それぞれを図にしてみたものが以下です。左が PHP 5.6.14, 右が PHP 7.0.0 RC4 です。それぞれのオペコードの右側に、PHP のコードとの対応を示しています。PHP 7.0.0 RC4 では、ループの終了判定を末尾にもっていくことで、見通しのよい流れになっています。ループ内の命令数を数えてみると、PHP 5.6.14 での 6 命令に対して PHP 7.0.0 RC4 では 4 命令まで削減されています。このように、同じ PHP のコードでもより効率的なオペコードを生成できるようになっています。

コンパイラのこのような工夫は他の言語でも一般的に行われています。例として gcc で確認してみます。PHP のプログラムと同等な次のプログラムをコンパイルします。
int main() { int i, x; for (i = 0; i < 10; ++i) { x = 1; } return 0; }
コンパイル結果は以下のようになりました。-S でアセンブリを出力、-O0 で gcc の最適化を行わないように指示しています。コメントを付けた 7 つのアセンブリ命令が PHP 7.0.0 RC4 でのオペコードに一対一に対応しており、同じ構造になっていることがわかります。
$ gcc -S -O0 test.c
$ cat test.s
(主要な部分のみ抜粋します)
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, -4(%rbp) // i = 0;
jmp .L2 // .L2 に無条件ジャンプ
.L3:
movl $1, -8(%rbp) // x = 1;
addl $1, -4(%rbp) // ++i;
.L2:
cmpl $9, -4(%rbp) // i と 9 を比較
jle .L3 // i <= 9 なら .L3 にジャンプ
movl $0, %eax // 戻り値 = 0
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc