PHPのfor文の高速化の話題で、教科書等によく記載されている

for($i = 0; $i < 10 $i++){
	//処理を書く
}

ではなく、

for($i = 0; $i < 10 ++$i){
	//処理を書く
}

にした方が良いということを時々見かける。

実際のところはどうなのか?を処理速度の測定とVLDで確認してみる。

VLDでPHPのオペコードを確認する


ちなみに$i++と++$iの違いは、if文等で、式を実行してから$iに1を加える(インクリメント)か、$iに1を加えてから式を実行するか?の違いがある。


$i = 2;
if($i++ > 2){
	//通過しない
}

では、2行目のif文の式の箇所がfalse(偽)になるが、

$i = 2;
if(++$i > 2){
	//通過する
}

であればtrue(真)になる。

インクリメント - Wikipedia




環境

Ubuntu 20.04

PHP 7.4.6


まずは処理速度の方を確認する。

これからの話はすべてファイル名をfor.phpで話を進める。


前者の$i++の方のコードは下記のようにする。

<?php
$start = microtime(true);
for($i = 0; $i < 100000000; $i++){
	//処理を書く
}
$end = microtime(true);
echo $end - $start;

実行と結果は下記の通り

$ php /path/to/dir/for.php
0.51271295547485

続いて、++$iの方を確認してみる。

<?php
$start = microtime(true);
for($i = 0; $i < 100000000; ++$i){
	//処理を書く
}
$end = microtime(true);
echo $end - $start;

$ php /path/to/dir/for.php
0.29786896705627

後者のコードにするだけで、処理速度が3/5程になった。




オペコードはどうなるのか?を確認してみる。

<?php
for($i = 0; $i < 10; $i++){
        //処理を書く
}

実行と結果は下記の通り

$ php -d vld.active=1 -d vld.execute=0 /path/to/dir/for.php
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 42) Position 1 = 4
Branch analysis from position: 4
2 jumps found. (Code = 44) Position 1 = 6, Position 2 = 2
Branch analysis from position: 6
1 jumps found. (Code = 62) Position 1 = -2
Branch analysis from position: 2
2 jumps found. (Code = 44) Position 1 = 6, Position 2 = 2
Branch analysis from position: 6
Branch analysis from position: 2
filename:       /path/to/dir/for.php
function name:  (null)
number of ops:  7
compiled vars:  !0 = $i
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
    2     0  E >   ASSIGN                                                   !0, 0
          1      > JMP                                                      ->4
          2    >   POST_INC                                         ~2      !0
          3        FREE                                                     ~2
          4    >   IS_SMALLER                                       ~3      !0, 10
          5      > JMPNZ                                                    ~3, ->2
    5     6    > > RETURN                                                   1

branch: #  0; line:     2-    2; sop:     0; eop:     1; out0:   4
branch: #  2; line:     2-    2; sop:     2; eop:     3; out0:   4
branch: #  4; line:     2-    2; sop:     4; eop:     5; out0:   6; out1:   2; out2:   6; out3:   2
branch: #  6; line:     5-    5; sop:     6; eop:     6; out0:  -2
path #1: 0, 4, 6, 
path #2: 0, 4, 2, 4, 6, 
path #3: 0, 4, 2, 4, 6, 
path #4: 0, 4, 6, 
path #5: 0, 4, 2, 4, 6, 
path #6: 0, 4, 2, 4, 6, 

オペコードに関する細かい話は置いといて、続いて下記のコードの方を確認する。

<?php
for($i = 0; $i < 10; ++$i){
        //処理を書く
}

実行と結果は下記の通り

$ php -d vld.active=1 -d vld.execute=0 /path/to/dir/for.php
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 42) Position 1 = 3
Branch analysis from position: 3
2 jumps found. (Code = 44) Position 1 = 5, Position 2 = 2
Branch analysis from position: 5
1 jumps found. (Code = 62) Position 1 = -2
Branch analysis from position: 2
2 jumps found. (Code = 44) Position 1 = 5, Position 2 = 2
Branch analysis from position: 5
Branch analysis from position: 2
filename:       /path/to/dir/for.php
function name:  (null)
number of ops:  6
compiled vars:  !0 = $i
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
    2     0  E >   ASSIGN                                                   !0, 0
          1      > JMP                                                      ->3
          2    >   PRE_INC                                                  !0
          3    >   IS_SMALLER                                       ~3      !0, 10
          4      > JMPNZ                                                    ~3, ->2
    5     5    > > RETURN                                                   1

branch: #  0; line:     2-    2; sop:     0; eop:     1; out0:   3
branch: #  2; line:     2-    2; sop:     2; eop:     2; out0:   3
branch: #  3; line:     2-    2; sop:     3; eop:     4; out0:   5; out1:   2; out2:   5; out3:   2
branch: #  5; line:     5-    5; sop:     5; eop:     5; out0:  -2
path #1: 0, 3, 5, 
path #2: 0, 3, 2, 3, 5, 
path #3: 0, 3, 2, 3, 5, 
path #4: 0, 3, 5, 
path #5: 0, 3, 2, 3, 5, 
path #6: 0, 3, 2, 3, 5, 

各々の結果で太字の箇所が違いだけれども、$i++から++$iにすると、オペコードが一行減っている。

おそらく、メモリの使用量でも少し削減されているはずだ。

for内の三番目の処理では、$i++と++$iのどちらも意味は変わらないので、積極的に++$iの方を使っていった方が良さそうだ。


余談

$i++の方にあるFREEのコードは内部で確保した値の解放という意味があるらしい。IS_SMALLERは2つの値の比較の処理(ここでは0行目で定義した変数の値とfor内の二番目の値の10の比較)なので、処理前に値を確保するためのメモリを確保しているのだろうか?

PHPの処理において、メモリの確保の処理は遅くなる傾向があるらしいので、++$iの方が速いのは、メモリの確保と解放が無いことに因るものでありそうだ。

PHP: FREE - Manual

PHP: IS_SMALLER - Manual