この記事は PHP Advent Calendar 2021 - Qiita の7日目です。
※2021/12/7 22:59追記 ブログ執筆初期と、結論が変わっております。お気をつけください。
抽象構文木 - Wikipedia
AST(抽象構文木)というものがあります。プログラミング言語においては、コンパイラやインタプリタが最終的に実行可能なコードに変換を行う前の中間表現として使われていたりします。
我らがPHPもバージョン7.0から、コンパイルの中間表現としてASTを生成するようになっています。詳しくは下記スライドを参照。
最近では、このASTをレビューに役立てたり、ソースコードに意味的な変更が行われたかどうかを検出する方法の一つとして利用されていたりします。
20 万行超のコードベースをテストせずにリファクタリングリリースした話 - MonotaRO Tech Blog
私が所属している会社でも、ASTはレビュープロセスの一つとして使われていまして、なんだか世の中進んでいるねという感じです。
発端
ある日、PHPにおける else if
と elseif
が異なるASTを出力していることに気が付きました。こういう細かいことって、調べてみると面白いので調べてみました。
忙しい貴方のための結論
とてつもなく、CPUバウンドな負荷にお困りで、パフォーマンスがとてつもなく大事な現場の方は、elseif
を使ったら良いです。ちょっとだけパフォーマンスが良いです。
最適化後のOPCodeは両者で一致するので、パフォーマンスの変化は無いです。
Xdebugが有効な場合 -> elseif
にしたほうが速い。これは、ステップ実行用のOPCodeが差し込まれるためです。本番でXdebug有効なことは無いと思うので、実際には無いケースだと思います。
サンプルソースコード
Loopで回して、偶数・奇数をチェックするような感じのわざとらしいコードです。
else if
1 2 3 4 5 6 7 8
| <?php $n = 30000000;
for($i=0; $i < $n ; $i++){ if($i%2 === 0){ }else if ($i%2 === 1){ } }
|
elseif
1 2 3 4 5 6 7 8
| <?php $n = 30000000;
for($i=0; $i < $n ; $i++){ if($i%2 === 0){ }elseif ($i%2 === 1){ } }
|
ASTの比較
利用するのは、PHPerおなじみのこちら。
GitHub - nikic/php-ast: Extension exposing PHP 7 abstract syntax tree
else if
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| AST_STMT_LIST 0: AST_ASSIGN var: AST_VAR name: "n" expr: 30000000 1: AST_FOR init: AST_EXPR_LIST 0: AST_ASSIGN var: AST_VAR name: "i" expr: 0 cond: AST_EXPR_LIST 0: AST_BINARY_OP flags: BINARY_IS_SMALLER (20) left: AST_VAR name: "i" right: AST_VAR name: "n" loop: AST_EXPR_LIST 0: AST_POST_INC var: AST_VAR name: "i" stmts: AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_BINARY_OP flags: BINARY_IS_IDENTICAL (16) left: AST_BINARY_OP flags: BINARY_MOD (5) left: AST_VAR name: "i" right: 2 right: 0 stmts: AST_STMT_LIST 1: AST_IF_ELEM cond: null stmts: AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_BINARY_OP flags: BINARY_IS_IDENTICAL (16) left: AST_BINARY_OP flags: BINARY_MOD (5) left: AST_VAR name: "i" right: 2 right: 1 stmts: AST_STMT_LIST
|
elseif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| AST_STMT_LIST 0: AST_ASSIGN var: AST_VAR name: "n" expr: 30000000 1: AST_FOR init: AST_EXPR_LIST 0: AST_ASSIGN var: AST_VAR name: "i" expr: 0 cond: AST_EXPR_LIST 0: AST_BINARY_OP flags: BINARY_IS_SMALLER (20) left: AST_VAR name: "i" right: AST_VAR name: "n" loop: AST_EXPR_LIST 0: AST_POST_INC var: AST_VAR name: "i" stmts: AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_BINARY_OP flags: BINARY_IS_IDENTICAL (16) left: AST_BINARY_OP flags: BINARY_MOD (5) left: AST_VAR name: "i" right: 2 right: 0 stmts: AST_STMT_LIST 1: AST_IF_ELEM cond: AST_BINARY_OP flags: BINARY_IS_IDENTICAL (16) left: AST_BINARY_OP flags: BINARY_MOD (5) left: AST_VAR name: "i" right: 2 right: 1 stmts: AST_STMT_LIST
|
特徴的な部分のみの比較
要するに何が起きているのかというと…
else if
if〜else
で文が区切られて、その後にもう一度 if
が入っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 1: AST_IF_ELEM cond: null stmts: AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_BINARY_OP flags: BINARY_IS_IDENTICAL (16) left: AST_BINARY_OP flags: BINARY_MOD (5) left: AST_VAR name: "i" right: 2 right: 1 stmts: AST_STMT_LIST
|
elseif
elseif
で一つの文として比較が完了しています。なのでASTが一段浅い。
1 2 3 4 5 6 7 8 9 10
| 1: AST_IF_ELEM cond: AST_BINARY_OP flags: BINARY_IS_IDENTICAL (16) left: AST_BINARY_OP flags: BINARY_MOD (5) left: AST_VAR name: "i" right: 2 right: 1 stmts: AST_STMT_LIST
|
つまり、こういうこと
if else if
else
節の後ろに、もう一つ if
文が入っています。
1 2 3 4 5 6 7 8
| if($cond){
}else { if($cond2){
} }
|
if elseif
elseif
節一つで、条件分岐が終わっています。
1 2 3 4 5
| if($cond){
}elseif($cond2){
}
|
OPCode最適化の影響チェック
ASTは、あくまで中間表現です。ということは、コンパイルで最適化が行われた後であれば、どちらの表現でも変わらないのではないか?
OPCacheを使うと、最適化前後のOPCodeを出力することが出来ます。実際に、最適化前後で出力して比較してみます。
else if
OPCode最適化前
※2021/12/7 22:59追記 Xdebugを無効化してやり直しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| $ php -dopcache.enable_cli=1 -dopcache.opt_debug_level=0x10000 ./code_1.php
$_main: ; (lines=14, args=0, vars=2, tmps=8) ; (before optimizer) ; /php_if_else/code_1.php:1-8 ; return [] RANGE[0..0] 0000 ASSIGN CV0($n) int(30000000) 0001 ASSIGN CV1($i) int(0) 0002 JMP 0011 0003 T4 = MOD CV1($i) int(2) 0004 T5 = IS_IDENTICAL T4 int(0) 0005 JMPZ T5 0007 0006 JMP 0010 0007 T6 = MOD CV1($i) int(2) 0008 T7 = IS_IDENTICAL T6 int(1) 0009 JMPZ T7 0010 0010 PRE_INC CV1($i) 0011 T9 = IS_SMALLER CV1($i) CV0($n) 0012 JMPNZ T9 0003 0013 RETURN int(1)
|
OPCode最適化後
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| $ php -dopcache.enable_cli=1 -dopcache.opt_debug_level=0x20000 ./code_1.php
$_main: ; (lines=12, args=0, vars=2, tmps=2) ; (after optimizer) ; /php_if_else/code_1.php:1-8 0000 ASSIGN CV0($n) int(30000000) 0001 ASSIGN CV1($i) int(0) 0002 JMP 0009 0003 T3 = MOD CV1($i) int(2) 0004 T2 = IS_IDENTICAL T3 int(0) 0005 JMPNZ T2 0008 0006 T3 = MOD CV1($i) int(2) 0007 T2 = IS_IDENTICAL T3 int(1) 0008 PRE_INC CV1($i) 0009 T2 = IS_SMALLER CV1($i) CV0($n) 0010 JMPNZ T2 0003 0011 RETURN int(1)
|
elseif
OPCode最適化前
※2021/12/7 22:59追記 Xdebugを無効化してやり直しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| $ php -dopcache.enable_cli=1 -dopcache.opt_debug_level=0x10000 ./code_2.php
$_main: ; (lines=14, args=0, vars=2, tmps=8) ; (before optimizer) ; /php_if_else/code_2.php:1-8 ; return [] RANGE[0..0] 0000 ASSIGN CV0($n) int(30000000) 0001 ASSIGN CV1($i) int(0) 0002 JMP 0011 0003 T4 = MOD CV1($i) int(2) 0004 T5 = IS_IDENTICAL T4 int(0) 0005 JMPZ T5 0007 0006 JMP 0010 0007 T6 = MOD CV1($i) int(2) 0008 T7 = IS_IDENTICAL T6 int(1) 0009 JMPZ T7 0010 0010 PRE_INC CV1($i) 0011 T9 = IS_SMALLER CV1($i) CV0($n) 0012 JMPNZ T9 0003 0013 RETURN int(1)
|
OPCode最適化後
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| $ php -dopcache.enable_cli=1 -dopcache.opt_debug_level=0x20000 ./code_2.php
$_main: ; (lines=12, args=0, vars=2, tmps=2) ; (after optimizer) ; /php_if_else/code_2.php:1-8 0000 ASSIGN CV0($n) int(30000000) 0001 ASSIGN CV1($i) int(0) 0002 JMP 0009 0003 T3 = MOD CV1($i) int(2) 0004 T2 = IS_IDENTICAL T3 int(0) 0005 JMPNZ T2 0008 0006 T3 = MOD CV1($i) int(2) 0007 T2 = IS_IDENTICAL T3 int(1) 0008 PRE_INC CV1($i) 0009 T2 = IS_SMALLER CV1($i) CV0($n) 0010 JMPNZ T2 0003 0011 RETURN int(1)
|
ようするに
OPCodeなんて、長々と見せられても良く分からんとおもいますので、かいつまんで説明します。
最適化後のOPCodeにおいて、両者の違いは EXT_STMT
の数です。else if
の方が1つだけ文の数が多いということになります。意外でしたが、最適化が行われた後でも両者には違いがありました。
※2021/12/7 22:59追記 Xdebugを無効化したことで、 EXT_STMT
が挿入されることがなくなり、両者は最適化レベルの差に依らず同じOPCodeを出力するようになりました。
ということで、みんな大好きマイクロベンチを取ります。
マイクロベンチ
30000000件の繰り返しによるテストです。
※2021/12/7 22:59追記 Xdebugを無効化してやり直しました。
else if
1 2 3 4 5
| $ time php code_1.php
real 0m2.280s user 0m2.217s sys 0m0.046s
|
elseif
1 2 3 4 5
| $ time php code_2.php
real 0m2.249s user 0m2.205s sys 0m0.036s
|
elseif
の方が、10%程度性能が良いという結果です。まあ、Webアプリではこんな回数のLoopを行わないので、問題にならないでしょうが、割と性能差が出て驚きです。
※2021/12/7 22:59追記 OPCodeが同じなので、実行時間の差は誤差程度に収まるようになりました。同じだから、そりゃそうです。
まとめ
Webアプリにおいては問題にならないでしょうが、PHPを使ってCPUを酷使するようなプログラムを行っている人におかれましては、置き換え可能であれば、else if を elseif にすると良いかもしれません。
一見同じに見える、ソースコードも、深く追ってみると色々と発見があって面白いですね。
※2021/12/7 22:59追記 というわけで、ASTは異なっていても、OPCodeの時点で両者は等価に変換されるので、どっちで書いても問題なし!!!!
ただし、ローカル環境でXdebugが有効化されている状態だと、パフォーマンスに差が出ます。お気をつけください。
追加調査
EXT_STMT
を差し込まれている理由について、詳細調査を追加でやりますので、そのうちブログにまとめます。