徹底検証!PHP最適化Tips

第4回制御構造、for文にまつわる噂を検証

はじめに

今回はfor文に関するtipsについて解説していきます。

for文の条件式にはcount($array)のような関数をいれない(変数に格納)

for文の条件式に関数を入れると遅くなるというtipsです。

例えば配列の要素数だけfor文を用いてループを回したいときに、配列の要素数を取得できるcount()関数を使って下記のように書くことができます。

for ($j=0; $j<count($array); $j++) {}

このtipsでは下記のようにあらかじめ変数に格納してからの使用を勧めてます。

$count = count($array);
for ($j=0; $j<$count; $j++) {}

まずはサンプルプログラムを用意し、ベンチマークをとってみます。

benchmark_for.php
<?php
$t = microtime(true);
$i = 0;
$a = null;
$array = array(1,2,3,4,5,6,7,8,9,10);
while ($i < 1000) {
   $count = count($array);
   for ($j=0;$j<$count;$j++) {
   }
   ++$i;        
}
$tmp = microtime(true) - $t;
var_dump($tmp);
?>
benchmark_for_use_count.php
<?php
$t = microtime(true);
$i = 0;
$a = null;
$array = array(1,2,3,4,5,6,7,8,9,10);
while($i < 1000) {
   for ($j=0;$j<count($array);$j++) {
   }
   ++$i;        
}
$tmp = microtime(true) - $t;
var_dump($tmp);
?>
ベンチマーク結果
$ php benchmark_for.php
float(0.00143694877625)
$ php benchmark_for_use_count.php
float(0.00325417518616)

tips通りfor文の条件式に関数を用いないほうが速い結果となりました。それではどうしてこのような結果になるのか検証していきたいと思います。

vldを用いた解析

今回ははじめにvldを用いて解析を行います。vld(Vulcan Logic Disassembler)はPHP用のディスアセンブラで、opcodeレベルでのPHPコードの解析に役立ちます。

まずは、vldをインストールします。

$ tar xzf vld-0.9.1.tgz
$ cd vld-0.9.1/
$ phpize
$ ./configure
$ make
$ sudo make install

上記の手順でインストールすることができます。

それではvldを使ってみます。サンプルコードとして簡単なfor文のコードを用意します。

1 <?php
2 for ($i=0;$i<count(1);$i++) {
3     echo '';
4 }
5 ?>

実行方法はvld.active=1をphp.iniに設定するか、コマンドラインオプションに付け加えるとvldが有効になります。

kajidai@laputa:~$ php -dvld.active=1 e.php
Branch analysis from position: 0
Jump found. Position 1 = 10, Position 2 = 8
Branch analysis from position: 10
Return found
Branch analysis from position: 8
Jump found. Position 1 = 5
Branch analysis from position: 5
Jump found. Position 1 = 1
Branch analysis from position: 1
filename:       /home/kajidai/e.php
function name:  (null)
number of ops:  12
compiled vars:  !0 = $i
line     #  op                           fetch          ext  return  operands
-------------------------------------------------------------------------------
  2     0  ASSIGN                                                   !0, 0
        1  SEND_VAL                                                 1
        2  DO_FCALL                                      1          'count'
        3  IS_SMALLER                                       ~2      !0, $1
        4  JMPZNZ                                        8          ~2, ->10
        5  POST_INC                                         ~3      !0
        6  FREE                                                     ~3
        7  JMP                                                      ->1
  3     8  ECHO                                                     ''
  4     9  JMP                                                      ->5
  6    10  RETURN                                                   1
       11* ZEND_HANDLE_EXCEPTION
               

実行結果は上記のようになりました。それでは、出力されたopcodeをもとに解析していきましょう。

2:DO_FCALLでcount()が実行されます。その結果をもとに3:IS_SMALLERで比較が行われ、 4:JMPZNZでは3:IS_SMALLERの結果がfalseであれば10:RETURNにジャンプし終了します。trueであればextended valueに設定されてる8:ECHOまで処理がジャンプし、forループ内のステートメントが実行され、9:JMPで5へジャンプしPOST_INCが実行され$iがインクリメントされます。そして7:JMPで1:SEND_VALへ処理がジャンプし再び2:DO_FCALLでcount()が実行されます。

このようにforループが続く間、条件式の評価が行われるたびにcount()が実行されることになります。

次に、実装を見てどこでopcodeが設定されているかPHPのコードを読んでいきましょう。

191     |   T_FOR
192             '('
193                 for_expr
194             ';' { zend_do_free(&$3 TSRMLS_CC); $4.u.opline_num = get_next_op_number(CG(active_op_array)); }
195                 for_expr
196             ';' { zend_do_extended_info(TSRMLS_C); zend_do_for_cond(&$6, &$7 TSRMLS_CC); }
197                 for_expr
198             ')' { zend_do_free(&$9 TSRMLS_CC);  zend_do_for_before_statement(&$4, &$7 TSRMLS_CC); }
199             for_statement { zend_do_for_end(&$7 TSRMLS_CC); }

上記はphp-5.2.6/Zend/zend_language_parser.yに書かれてるfor文の構文解析部分のコードになります。

“for(A;B;C){D}⁠を用いて説明すると、193行目ではAが処理され、195行目では条件式Bが処理され、197行目ではCが処理され、199行目ではfor文内で実行されるステートメントDの処理されます。for文は191行目から順に解析されopcodeが登録されていきます。

続いて、for文の解析時に呼ばれる代表的な関数について説明を行います。

1548 void zend_do_end_function_call(znode *function_name, znode *result, znode *argument_list, int is_method, in     t is_dynamic_fcall TSRMLS_DC)
1549 {
1550     zend_op *opline;
1551 
1552     if (is_method && function_name && function_name->op_type == IS_UNUSED) {
1553         /* clone */
1554         if (Z_LVAL(argument_list->u.constant) != 0) {
1555             zend_error(E_WARNING, "Clone method does not require arguments");
1556         }
1557         opline = &CG(active_op_array)->opcodes[Z_LVAL(function_name->u.constant)];
1558     } else {
1559         opline = get_next_op(CG(active_op_array) TSRMLS_CC);
1560         if (!is_method && !is_dynamic_fcall && function_name->op_type==IS_CONST) {
1561             opline->opcode = ZEND_DO_FCALL;
1562             opline->op1 = *function_name;
1563         } else {
1564             opline->opcode = ZEND_DO_FCALL_BY_NAME;
1565             SET_UNUSED(opline->op1);
1566         }
1567     }

zend_do_end_function_call()はcount()部分の解析時に呼ばれます。1561行目でZEND_DO_FCALLがopcodeに設定され、1562行目でop1にcount関数の実行がznodeとして設定されます。

736 void zend_do_for_cond(znode *expr, znode *second_semicolon_token TSRMLS_DC)
737 {
738     int for_cond_op_number = get_next_op_number(CG(active_op_array));
739     zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
740 
741     opline->opcode = ZEND_JMPZNZ;
742     opline->op1 = *expr;  /* the conditional expression */
743     second_semicolon_token->u.opline_num = for_cond_op_number;
744     SET_UNUSED(opline->op2);
745 }

zend_do_for_cond()では741行目でopcodeにZEND_JMPZNZが設定されます。

748 void zend_do_for_before_statement(znode *cond_start, znode *second_semicolon_token TSRMLS_DC)
749 {
750     zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
751 
752     opline->opcode = ZEND_JMP;
753     opline->op1.u.opline_num = cond_start->u.opline_num;
754     CG(active_op_array)->opcodes[second_semicolon_token->u.opline_num].extended_value = get_next_op_number(     CG(active_op_array));
755     SET_UNUSED(opline->op1);
756     SET_UNUSED(opline->op2);
757 
758     do_begin_loop(TSRMLS_C);
759 
760     INC_BPC(CG(active_op_array));
761 }

zend_do_for_before_statement()ではopcodeにZEND_JMPが設定されます。ジャンプ先にはop1.u.opline_numにfor文の条件式部分が設定されます。

764 void zend_do_for_end(znode *second_semicolon_token TSRMLS_DC)
765 {
766     zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
767 
768     opline->opcode = ZEND_JMP;
769     opline->op1.u.opline_num = second_semicolon_token->u.opline_num+1;
770     CG(active_op_array)->opcodes[second_semicolon_token->u.opline_num].op2.u.opline_num = get_next_op_numbe     r(CG(active_op_array));
771     SET_UNUSED(opline->op1);
772     SET_UNUSED(opline->op2);
773 
774     do_end_loop(second_semicolon_token->u.opline_num+1, 0 TSRMLS_CC);
775 
776     DEC_BPC(CG(active_op_array));
777 }

zend_do_for_end()ではopcodeにZEND_JMPが、ジャンプ先にはfor(A;B;C)のCの部分が設定されます。

このように、先ほどvldで出力したopcodeとfor文の実行との関係が簡単ではありますがわかったと思います。

話がだいぶずれてしまいましたが、まとめるとfor文内の条件式に関数を設定すると、関数の実行が条件式が評価されるたびに起きるために遅くなってました。tips通りfor文内の条件式では関数を使わずにあらかじめ変数に格納して使ったほうがよいという結論になりました。

おわりに

これまで4回にわたってお送りした「徹底検証! PHP最適化Tips」の連載は今回で最後になります。PHP最適化TipsをもとにPHPのソースを読み、実装を知ることでその有効性について説明を行ってきました。

本連載をきっかけにさらにPHPに興味を持っていただけたら幸いです。

おすすめ記事

記事・ニュース一覧