search
LoginSignup
4

posted at

updated at

ZendEngineにえこひいきされる標準関数たち (前篇)

こんにちは! えこひいきしてますか? 僕はしないようにしてます!

ところでPHPとかいう言語の標準ランタイム (つまりは、みなさんが普段使ってるphpコマンド) はPHPスクリプトを直接実行するのではなく、実行前にコンパイラがPHPスクリプトをオペコードと呼ばれる中間表現(バイトコード)にコンパイルしてから、ZendEngineと呼ばれるVM(仮想機械)がオペコードを実行する構成になっていますよね。

PHPスクリプトで関数呼び出しをすると、あたりまえですが関数呼び出しをする命令にコンパイルされます。これはまったく不自然ではないことです。ところが、PHPの標準関数にはその前提に従わない、つまりは特別扱いえこひいきされているものがあります。

今回の記事はそんなえこひいきされた関数たちを眺めていきましょう ヾ(〃><)ノ゙ :tada:

ふざけた導入に反して、この記事は比較的まともな技術的トピックを扱っているつもりです

前提: バイトコンパイルされるPHP

ここまでの前ふりの意味の理解に自信がなかったら、 @sj-i さんの「PHP による hello world 入門」、特に「Zend Engine とオペコード」の部分を読んできてください。

要約すると、

  • PHPの処理系はZend EngineというレジスタマシンベースのVM型インタプリタである
  • Zend EngineはPHPスクリプトをパースし、Zend Engineのオペコードにコンパイルした上で実行する
  • Zend Engineには150以上の命令があり、加算や乗算だけでなくPHPと密接に関連した複雑なものがある

この記事が書かれた頃(2015年)との差分としては

……とはいえ、vldはまだまだ便利です。どこで使うのかって?

ご存じでしたか?

スクリーンショット 2022-12-18 9.59.51.png

なんと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神に選ばれて神隠しに遭う対象の特別な関数だったのでした。

……というわけで、ここまでが今回の話題の前振りです。

次からは記事のタイトル通り「えこひいきされる標準関数たち」という話をしていくのですが、その前に最適化の恩寵を受けられない例を紹介しておきます。

namespaceを追加する
 <?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の呪いに対抗する方法は二つあります。

対抗策1:use function
 <?php
 namespace hoge;
+use function strlen;
+use function var_dump;
 var_dump(strlen('abc') + strlen('def'));
対抗策2:関数呼び出しに \ を付ける
 <?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 を付けるようにしていくのは良い習慣かもしれません。また、機械的に検査したり自動で付与するのもよいでしょう。

つづく

切りがいいので、関数ごとに具体的にどんなえこひいき 特殊な最適化 がされているかは後篇に続きます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4