Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 | 

2015年12月30日(水) PHP7からstrlen関数に特化した高速化が採用された このエントリーを含むブックマーク このエントリーのブックマークコメント

(2016/01/01 02:20追記)mbstring.func_overloadの章を盛大に書き換えました。なぜか廃止されたと思い込んでたんですが、特に廃止もされてなくて、PHP7でも動くことは動きます。ただ、仕組み上strlenだけ言うことを聞かなくなっていますので、使い道としては厳しいと思います。


みなさん、もうPHP7は試してみましたか?


PHP7のセールスポイントと言えば高速化ですよね。その高速化ですが、個人的には「そこ速くする余地あったの?」と思えるような箇所が高速化されていたりします。本稿では、そうした意外な高速化ポイントの一つとしてstrlen関数に関する高速化について紹介します。


strlen関数最適化

念のため説明しておきますと、strlen関数というのは文字列の長さを返す関数です。ところで、PHPでは文字列の長さはあらかじめ文字列本体とは別に格納されています(PHP文字列はヌル文字でターミネートされていないので、文字列長がないと文字列末尾がわかりません)。つまり、元々わかっている数字を返すだけですから、strlen関数自体は大した仕事はしません。


大した仕事をしていない関数最適化の対象になるというのは少し不思議な気もしますが、そういうわけではありません。実はPHP関数呼び出しはコストが比較的高いので、関数呼び出し自体が最適化の対象になっているというわけなのです。


実際に、PHP7では関数呼び出し自体のコストを下げるような最適化も行われました(参考:「PHP7調査(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引数を普通の引数と同じように評価するわけにはいかないので、式として保存するような処理をしてるんだろう、と想像がついたりするわけです。


mbstring.func_overloadが期待通りに動かなくなっている件とstrlen関数最適化

(ごめんなさい、この章は思い込みで書いていたので全面的に書き換えました…)


PHP 5まではmbstring.func_overloadを指定することでsubstr関数の呼び出しに対してmb_substr関数が呼ばれるような機能がありました。マルチバイト非対応のPHPアプリケーションを無理矢理マルチバイト対応にするための大昔のハックなんだと思います。


ところで、この機能は今回紹介した最適化と密接な関係があります。というのも、strlenもmbstringによる関数オーバーロードの対象だったからです。関数オーバーロード関数テーブルの置き換えにより実現されていましたが、今回のように関数呼び出しを行わないような最適化をしてしまうと同じ方法では挙動を変更できません。そのため、PHP7ではstrlenのオーバーロードは動かなくなっています。


まとめ

  • PHP7からstrlen関数のためだけにopcodeが1個増えた
  • PHP7では、一部の関数関数呼び出しされなくなった
  • mbstringのオシャレ機能mbstring.func_overloadは生き残っているが、strlen関数の置き換えが動かないので使い道がなさそう

sasezakisasezaki 2015/12/31 23:17 > そこで、「この機能って最適化の邪魔なんだけど、そもそも日本人使ってんの?」というような議論

これってどこで話されてました?参考リンクあります?

hnwhnw 2016/01/01 02:05 https://marc.info/?l=php-internals&m=142719755322933&w=2

この辺かなと。

hnwhnw 2016/01/01 02:32 その議論の末に廃止されたんだと思い込んでましたが、ちゃんと生き残ってましたね…。ご指摘ありがとうございました!記事も修正させて頂きました。

トラックバック - http://d.hatena.ne.jp/hnw/20151230
 | 
ページビュー
1949808