こんにちは! どんどんえこひいきしていきましょう!
というわけで、後篇のこの記事では最適化の恩恵が受けられる場合において、どのような関数がその対象になるのかを見ていきましょう。
この記事ではPHP 8.2.0を対象に紹介します
この記事ではZendEngine命令名のプレフィクス ZEND_
を省略しています (vldスタイル)
コードを読む
関数の最適化を担っているのは zend_try_compile_special_func
関数です。
プログラムを読める人ならこれを見てもらえれば何をやっているのかは一目瞭然なのですが、せっかくなので紹介していきましょう。
strlen()
言わずと知れた文字列長を返す関数です。より正確に言うならバイトシーケンスの長さを返すのですが、今回の最適化とは直接関係がないので置いておきましょう。
この関数は引数が strlen('str')
のようなリテラルだったらその長さ(つまり3
)に展開します。そうでない場合(変数や式など)の場合はSTRLEN
という専用命令にコンパイルします。また、strlen()
自体が内部的には別にちまちまバイト数を数えてるわけじゃなくてzend_string
構造体のメンバー変数len
を返してるだけなので遅くはないです。
INIT_FCALL 'var_dump'
のように文字列で関数コールするのと専用命令があるのは電話番号をポチポチ押して電話を掛けるのと、受話器を上げるだけで繋がる専用線で直通しているくらい違う気がします。いや、言いすぎか…
is_*()
型チェック関数
is_null()
, is_bool()
, is_int()
, is_float()
, is_string()
, is_array()
, is_object()
, is_resource()
, is_scalar()
が該当します。
関数呼び出しをZEND_TYPE_CHECK
命令に置き換えます。
*val()
型キャスト関数
boolval()
, intval()
, floatval()
, strval()
が該当します。
ZEND_CAST
命令(boolのみZEND_BOOL
)に置き換えます。もちろんこれはキャスト構文(string)$val
と同じ結果なので、strval($val)
と書くべきかは単純に好みの問題だといえます。
強いていうなら(string)
の方が use function strval;
と書かなくてもパフォーマンス低下の心配がない
chr()
chr()
は8ビットの数値(1〜255)を1バイトのバイナリ(文字列)に変換する関数です。
chr(97)
のように書くと、文字列の'a'
として展開されます。
たとえば echo chr(240) . chr(159) . chr(144) . chr(152);
は ECHO '🐘'
という命令に変換されます。
ord()
ord()
はバイナリ列(文字列)の先頭のバイトを8ビットの数値(1〜255)に変換する関数です。
ord('a')
のように書くと、intの97
として展開されます。
call_user_func()
/ call_user_func_array()
call_user_func()
と call_user_func_array()
は関数/メソッドを動的呼び出しする関数です。
いままでの処理と比べるとコードがちょっと長いのですが、ふたつの関数は共通処理が多いのでまとめて書きます。
- 呼び出しの関数名に文字列リテラルで指定されていれば
INIT_FCALL
に置き換え- つまり普通の関数呼び出しと同じ形にコンパイルする
- 関数名が固定でなければ
INIT_USER_CALL
命令に置き換え
call_user_func('var_dump', 1);
のように第一引数に関数を文字列で書いているとvar_dump(1);
と同じようなコードにコンパイルしてくれます。一瞬「すごーい」と思ったのですが、それ最初からvar_dump(1)
って書けばよくね…? となるので存在意義が不明ではあります。
近年では $f = 'var_dump'; $f(1);
や var_dump(...$args);
のように call_user_func()
/ call_user_func_array()
なしで関数の動的呼び出しが簡単になっています。
関数名固定・パラメータ可変のコンパイル結果比較 (折り畳み)
<?php
$args = [1];
call_user_func('var_dump', ...$args);
call_user_func_array('var_dump', $args);
var_dump(...$args);
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > ASSIGN !0, <array>
4 1 INIT_FCALL 'call_user_func'
2 SEND_VAL 'var_dump'
3 SEND_UNPACK !0
4 CHECK_UNDEF_ARGS
5 DO_ICALL
5 6 INIT_FCALL 'var_dump'
7 SEND_ARRAY !0
8 CHECK_UNDEF_ARGS
9 DO_FCALL 0
6 10 INIT_FCALL 'var_dump'
11 SEND_UNPACK !0
12 CHECK_UNDEF_ARGS
13 DO_ICALL
14 > RETURN 1
関数名可変・パラメータ固定のコンパイル結果比較 (折り畳み)
<?php
$f = 'var_dump';
call_user_func($f, 1);
call_user_func_array($f, [1]);
$f(1);
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'var_dump'
3 1 INIT_USER_CALL 1 'call_user_func', !0
2 SEND_USER 1
3 DO_FCALL 0
4 4 INIT_USER_CALL 0 'call_user_func_array', !0
5 SEND_ARRAY <array>
6 CHECK_UNDEF_ARGS
7 DO_FCALL 0
5 8 INIT_DYNAMIC_CALL !0
9 SEND_VAL_EX 1
10 DO_FCALL 0
11 > RETURN 1
INIT_FCALL
, INIT_USER_CALL
, INIT_DYNAMIC_CALL
のパフォーマンス的な比較はできていないので本稿においてはコンパイル結果の比較に留めておきます。
in_array()
in_array()
は要素が配列に含まれるかどうかをチェックする関数です。
コードの書きかたが若干複雑なのですが、
-
in_array($v, [1, 2, 3], strict: true)
のようにstrict: true
で配列がリテラルで要素が文字列または整数 -
in_array($v, ['a', 'b', 'c'])
のように配列がリテラルで要素がすべて文字列
このどちらかの条件のとき、内部的にハッシュテーブルを構築した上でIN_ARRAY
命令にコンパイルすることで高速に探索できるようになっています。
いま私はこの記事を書きながらin_array()
の最適化を知って感動しています。
この最適化はDmitry Stogovさん1によって2017年にPHP 7.2.0で導入されていました。
なお、IN_ARRAY
命令を使わない場合のin_array()
とisset
の比較については以下の記事のコメントに書きました。
count()
count()
は配列やオブジェクトの要素数を返す関数です。 エイリアス: sizeof()
これは引数の数が変じゃない限りは無条件でCOUNT
命令に置換しています。
せっかくなので実行時に呼ばれる COUNT
命令のハンドラの方も読んでみましょう。
これエラーハンドリングなんですが。
zend_type_error("%s(): Argument #1 ($value) must be of type Countable|array, %s given", opline->extended_value ? "sizeof" : "count", zend_zval_type_name(op1));
……なんだかあたかも関数からエラーが出たかのように偽装されていて微笑ましいですね。
get_class()
/ get_called_class()
/ gettype()
それぞれ別物の機能なのですが、最適化の実装は似ているのでまとめて書きます。
引数の数が間違ってない限り、GET_CLASS
, GET_CALLED_CLASS
, GET_TYPE
命令に置き換える。以上です。
func_num_args()
/ func_get_args()
PHP 7でパラメータ宣言に...
が導入される前に任意個の引数をとりたいときに使われていた機能です。
おもしろみがないのですが、これも FUNC_NUM_ARGS
、 FUNC_GET_ARGS
命令にコンパイルしているだけです。
array_slice()
配列の一部を切り取る関数です。
C言語のコードなんて見たらわかるだろ… と思ったらわからない。なんでこんなことに…
array_key_exists()
配列にキーが存在するかを返す関数です。
これは関数呼び出しをARRAY_KEY_EXISTS
命令に変換します。
大昔は「isset
は言語構造だから高速だがarray_key_exists()
は関数だから重い」というのはPHPのパフォーマンス鉄板ネタだったのですが、PHP 7.4.0から専用命令になったので現在では大きな差はなくなっているはずです。
isset($array['key'])
とarray_key_exists('key', $array)
は記法の違いの好みだけでなく機能が異なる(null
の扱いが違う)のですが、すくなくとも isset($array['key']) || array_key_exists('key', $array)
のようなことをしてパフォーマンスを稼ぐ必要はなくなったのです。21世紀の文明だ。
まとめ
Zend Engineにひいきしてもらったところでパフォーマンス改善に役立つかというと、損はしないがこれ目当てでもボトルネックを削減できるようなパターンになることは稀な、微妙な技です。フレームワークだったりテンプレートエンジンだったり、あと頻繁にデータにアクセスするようなツールだと効くようなこともあるかも。
あと、せっかくなのでPHP勉強会@東京でLT発表してきました。ということで以下の記事に続く。
-
PHPバス係数の1。参考: インフィニットループは PHP の継続的な発展を目指す The PHP Foundation に寄付をしました ↩