こんにちは! えこひいきしてますか? 僕はしないようにしてます!
ところでPHPとかいう言語の標準ランタイム (つまりは、みなさんが普段使ってるphp
コマンド) はPHPスクリプトを直接実行するのではなく、実行前にコンパイラがPHPスクリプトをオペコードと呼ばれる中間表現(バイトコード)にコンパイルしてから、ZendEngineと呼ばれるVM(仮想機械)がオペコードを実行する構成になっていますよね。
PHPスクリプトで関数呼び出しをすると、あたりまえですが関数呼び出しをする命令にコンパイルされます。これはまったく不自然ではないことです。ところが、PHPの標準関数にはその前提に従わない、つまりは特別扱いされているものがあります。
今回の記事はそんなえこひいきされた関数たちを眺めていきましょう ヾ(〃><)ノ゙
ふざけた導入に反して、この記事は比較的まともな技術的トピックを扱っているつもりです
前提: バイトコンパイルされるPHP
ここまでの前ふりの意味の理解に自信がなかったら、 @sj-i さんの「PHP による hello world 入門」、特に「Zend Engine とオペコード」の部分を読んできてください。
要約すると、
- PHPの処理系はZend EngineというレジスタマシンベースのVM型インタプリタである
- Zend EngineはPHPスクリプトをパースし、Zend Engineのオペコードにコンパイルした上で実行する
- Zend Engineには150以上の命令があり、加算や乗算だけでなくPHPと密接に関連した複雑なものがある
この記事が書かれた頃(2015年)との差分としては
- ZendEngine 4.x (PHP 8.x系)の命令数は202個
- オペコードを確認する方法として、vldもphpdbgも要らなくなった
……とはいえ、vldはまだまだ便利です。どこで使うのかって?
ご存じでしたか?
なんと3v4lでPHPスクリプトを実行するとコンパイル結果を確認できるのです。
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > ASSIGN !0, 123
4 1 INIT_FCALL 'var_dump'
2 ADD ~2 !0, 456
3 SEND_VAL ~2
4 DO_ICALL
5 > RETURN 1
上記のオペコードの元コードも並べてみましょう。
<?php
$a = 123;
var_dump($a + 456);
$a
という変数名は !0
に置き換わっていますが、対応がおわかりいただけますでしょうか。 INIT_FCALL 'var_dump'
は文字列で関数呼び出しをして、SEND_VAL
で引数を登録、DO_ICALL
で実行しています。
ところが、このコンパイルというのは、必ずしも1対1対応の結果になるわけではありません。
<?php
var_dump(123 + 456);
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > INIT_FCALL 'var_dump'
1 SEND_VAL 579
2 DO_ICALL
3 > RETURN 1
123 + 456
は、いつ世界のどこで実行しようと同じ結果になります。よってプログラムの実行時に毎回計算しなおすのは無駄があります。Zend Engineはこのような計算を毎回しなおすことがないように、579という計算結果に置き換えてくれるのです。
ここで+
はただ直訳するだけではない、コンパイル時に特別扱いされる対象だということです。
……勘のいい方はもう気付いたのではないでしょうか。
<?php
var_dump(strlen('abc') + strlen('def'));
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > INIT_FCALL 'var_dump'
1 SEND_VAL 6
2 DO_ICALL
3 > RETURN 1
どこへ行った INIT_FCALL 'strlen'
!
なんと、strlen()
はZend神に選ばれて神隠しに遭う対象の特別な関数だったのでした。
……というわけで、ここまでが今回の話題の前振りです。
次からは記事のタイトル通り「えこひいきされる標準関数たち」という話をしていくのですが、その前に最適化の恩寵を受けられない例を紹介しておきます。
<?php
+namespace hoge;
var_dump(strlen('abc') + strlen('def'));
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > INIT_NS_FCALL_BY_NAME 'hoge%5Cvar_dump'
1 INIT_NS_FCALL_BY_NAME 'hoge%5Cstrlen'
2 SEND_VAL_EX 'abc'
3 DO_FCALL 0 $0
4 INIT_NS_FCALL_BY_NAME 'hoge%5Cstrlen'
5 SEND_VAL_EX 'def'
6 DO_FCALL 0 $1
7 ADD ~2 $0, $1
8 SEND_VAL_EX ~2
9 DO_FCALL 0
10 > RETURN 1
PHPという言語はnamespaceに関数を定義できます。ということは、namespace hoge;
のファイルでstrlen()
という呼び出しをするとhoge\strlen()
という関数を呼び出そうとしているのか、トップレベルの strlen()
を呼び出そうとしているのか判別がつきません。
INIT_NS_FCALL_BY_NAME 'hoge%5Cvar_dump'
はnamespace内で、このように2段階で関数を検索して実行する命令です。厳格に対応がチェックされるクラスとは違って関数は細かいことを気にせずに呼び出しができるのはメリットですが、簡単に最適化のメリットを失なわせることもできる。
このようなnamespaceの呪いに対抗する方法は二つあります。
<?php
namespace hoge;
+use function strlen;
+use function var_dump;
var_dump(strlen('abc') + strlen('def'));
<?php
namespace hoge;
-var_dump(strlen('abc') + strlen('def'));
+\var_dump(\strlen('abc') + \strlen('def'));
これで INIT_NS_FCALL_BY_NAME
を避けて INIT_FCALL
に戻すことができるのです。
ここで朗報です。INIT_NS_FCALL_BY_NAME
はみなさんも無意識に日常的に起こしている非効率なコードだと思いますが、これがアプリケーションのボトルネックになることはほとんどないので、これまで書いてきたコードを思い出して不安がることはありません。
パフォーマンスチューニングを目的に手作業で\
を付けて回る作業は、一般的なWebアプリケーションにおける最適化の手段としては筋がよいとは言えません。暇なら止めはしませんが…
一方で非効率なコードを敢えて実行するメリットもないので、新規コードで use function
を付けるようにしていくのは良い習慣かもしれません。また、機械的に検査したり自動で付与するのもよいでしょう。
つづく
切りがいいので、関数ごとに具体的にどんなえこひいき 特殊な最適化 がされているかは後篇に続きます。