y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

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 のコードでもより効率的なオペコードを生成できるようになっています。
f:id:y_uti:20151008214511p:plain

コンパイラのこのような工夫は他の言語でも一般的に行われています。例として 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