PHP7からstrlen関数に特化した高速化が採用された
(2016/01/01 02:20追記)mbstring.func_overloadの章を盛大に書き換えました。なぜか廃止されたと思い込んでたんですが、特に廃止もされてなくて、PHP 7でも動くことは動きます。
(2017/5/14 追記)PHP 7でmbstring.func_overloadを有効にしてもstrlen()は期待通り動作しないと書いていましたが、期待通りmb_strlen()として動作していました。つまり、「2016/01/01 02:20追記」が一部嘘でしたので、記事を修正・追記しました。
みなさん、もうPHP 7は試してみましたか?
PHP 7のセールスポイントと言えば高速化ですよね。その高速化ですが、個人的には「そこ速くする余地あったの?」と思えるような箇所が高速化されていたりします。本稿では、そうした意外な高速化ポイントの一つとしてstrlen
関数に関する高速化について紹介します。
strlen関数と最適化
念のため説明しておきますと、strlen
関数というのは文字列の長さを返す関数です。ところで、PHPでは文字列の長さはあらかじめ文字列本体とは別に格納されています(PHPの文字列はヌル文字でターミネートされていないので、文字列長がないと文字列末尾がわかりません)。つまり、元々わかっている数字を返すだけですから、strlen
関数自体は大した仕事はしません。
大した仕事をしていない関数が最適化の対象になるというのは少し不思議な気もしますが、そういうわけではありません。実はPHPの関数呼び出しはコストが比較的高いので、関数呼び出し自体が最適化の対象になっているというわけなのです。
実際に、PHP 7では関数呼び出し自体のコストを下げるような最適化も行われました(参考:「PHP 7調査(16)高速な引数パーサの導入」)。しかし、strlen
関数については「そもそも関数呼び出しをしない」という最適化が採用されています。なんと、strlen
関数は関数呼び出しではなくZend VMの1命令に格上げになっています。
このことを確認してみましょう。vld拡張でstrlen
関数のコンパイル結果を確認してみると、見慣れないSTRLEN
というopcodeが見つかります。
$ php -dextension=vld.so -dvld.active=1 -r 'strlen($foo);' (略) line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 1 0 E > STRLEN ~1 !0 1 FREE ~1 2 > RETURN null (略) $
貴重なopcodeをこんなことで1個消費していいのか?他の関数では同じ最適化をしないのか?という疑問も湧いてきますが、頻繁に呼ばれる割に大したことをしていない数少ない関数ということなのかもしれません。
対応するCソースコードを確認する
PHP 7のZend/zend_compile.c
を見ると、次のように一部の関数を特別扱いしている処理が見つかります。
int zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, zend_function *fbc) /* {{{ */ { if (fbc->internal_function.handler == ZEND_FN(display_disabled_function)) { return FAILURE; } if (zend_string_equals_literal(lcname, "strlen")) { return zend_compile_func_strlen(result, args); } else if (zend_string_equals_literal(lcname, "is_null")) { return zend_compile_func_typecheck(result, args, IS_NULL); } else if (zend_string_equals_literal(lcname, "is_bool")) { return zend_compile_func_typecheck(result, args, _IS_BOOL); } else if (zend_string_equals_literal(lcname, "is_long") || zend_string_equals_literal(lcname, "is_int") || zend_string_equals_literal(lcname, "is_integer") ) { return zend_compile_func_typecheck(result, args, IS_LONG); } else if (zend_string_equals_literal(lcname, "is_float") || zend_string_equals_literal(lcname, "is_double") || zend_string_equals_literal(lcname, "is_real") ) { return zend_compile_func_typecheck(result, args, IS_DOUBLE); } else if (zend_string_equals_literal(lcname, "is_string")) { return zend_compile_func_typecheck(result, args, IS_STRING); } else if (zend_string_equals_literal(lcname, "is_array")) { return zend_compile_func_typecheck(result, args, IS_ARRAY); } else if (zend_string_equals_literal(lcname, "is_object")) { return zend_compile_func_typecheck(result, args, IS_OBJECT); } else if (zend_string_equals_literal(lcname, "is_resource")) { return zend_compile_func_typecheck(result, args, IS_RESOURCE); } else if (zend_string_equals_literal(lcname, "defined")) { return zend_compile_func_defined(result, args); } else if (zend_string_equals_literal(lcname, "call_user_func_array")) { return zend_compile_func_cufa(result, args, lcname); } else if (zend_string_equals_literal(lcname, "call_user_func")) { return zend_compile_func_cuf(result, args, lcname); } else if (zend_string_equals_literal(lcname, "assert")) { return zend_compile_assert(result, args, lcname, fbc); } else { return FAILURE; } } /* }}} */
たしかにstrlen
関数のための分岐があるのがわかります。
また、strlen
関数以外にもis_long
関数などが特別扱いされていることがわかります。実はこれらの関数も関数呼び出しを行わないような最適化が行われています。
また、assert
関数も特別扱いされていることがわかります。PHP 7からassert
関数の内側に任意の式を書けるようになったわけですが、assert
の引数を普通の引数と同じように評価するわけにはいかないので、式として保存するような処理をしてるんだろう、と想像がついたりするわけです。
strlen関数の最適化とmbstring.func_overloadの関係
(ごめんなさい、この章は思い込みで書いていたので全面的に書き換えました…)
PHPにはmbstring.func_overload
を指定することでsubstr
関数の呼び出しに対してmb_substr
関数が呼ばれるような機能があります。マルチバイト非対応のPHPアプリケーションを無理矢理マルチバイト対応にするための大昔のハックなんだと思います。
ところで、この機能は今回紹介した最適化と密接な関係があります。というのも、strlen
もmbstring関数への置き換え対象だからです。この機能は関数テーブルの置き換えにより実現されているのですが、今回のように別opcodeにコンパイルされてしまうと当然関数呼び出しは行われないため、関数の挙動を変更できません。
そこで、PHP 7ではmbstring.func_overload
が設定されていた場合にはstrlen
のopcode化は行われず、従来通り関数呼び出しの形でコンパイルされます。つまり、期待通りmb_strlen
が呼び出されることになります。