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

こんにちは！ えこひいきしてますか？ *僕はしないようにしてます！*

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

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

今回の記事はそんな**えこひいき**された関数たちを眺めていきましょう ヾ(〃＞＜)ﾉﾞ :tada: 

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

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

ここまでの前ふりの意味の理解に自信がなかったら、 @sj-i さんの「[PHP による hello world 入門](https://tech.respect-pal.jp/php-helloworld/)」、特に「Zend Engine とオペコード」の部分を読んできてください。

https://tech.respect-pal.jp/php-helloworld/

要約すると、

 * PHPの処理系は[Zend Engine](https://ja.wikipedia.org/wiki/Zend_Engine)という[レジスタマシン](https://ja.wikipedia.org/wiki/%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%B7%E3%83%B3)ベースのVM型インタプリタである
 * Zend EngineはPHPスクリプトをパースし、Zend Engineのオペコードにコンパイルした上で実行する
 * Zend Engineには150以上の命令があり、加算や乗算だけでなくPHPと密接に関連した複雑なものがある

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

 * **ZendEngine 4.x** (PHP 8.x系)の命令数は[202個](https://github.com/php/php-src/blob/70ee6c20ad97e02c2b8098aeea96fefbbc3ac5c2/Zend/zend_vm_opcodes.h#L86-L289)
 * オペコードを確認する方法として、[vldもphpdbgも要らなくなった](https://qiita.com/hnw/items/352c5030d6729343a49e)

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

https://3v4l.org/kU7ho

ご存じでしたか？

![スクリーンショット 2022-12-18 9.59.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4121/07016721-c552-c79b-9a4b-a2b09ee8b6d2.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
<?php

$a = 123;
var_dump($a + 456);
```

`$a` という変数名は `!0` に置き換わっていますが、対応がおわかりいただけますでしょうか。 `INIT_FCALL 'var_dump'`は文字列で関数呼び出しをして、`SEND_VAL`で引数を登録、`DO_ICALL`で実行しています。

ところが、このコンパイルというのは、必ずしも1対1対応の結果になるわけではありません。

```php
<?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という計算結果に置き換えてくれるのです。

:::note
このような最適化を「[部分評価](https://ja.wikipedia.org/wiki/%E9%83%A8%E5%88%86%E8%A9%95%E4%BE%A1)」、特に「**[定数の畳み込み](https://ja.wikipedia.org/wiki/%E5%AE%9A%E6%95%B0%E7%95%B3%E3%81%BF%E8%BE%BC%E3%81%BF)**」といいます
:::

ここで`+`はただ直訳するだけではない、コンパイル時に特別扱いされる対象だということです。

……勘のいい方はもう気付いたのではないでしょうか。

```php
<?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
```

https://3v4l.org/noXcI/vld

どこへ行った `INIT_FCALL 'strlen'` ！

https://dic.pixiv.net/a/%E4%B8%AD%E3%81%AB%E8%AA%B0%E3%82%82%E3%81%84%E3%81%BE%E3%81%9B%E3%82%93%E3%82%88

なんと、`strlen()`はZend神に選ばれて神隠しに遭う対象の特別な関数だったのでした。

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

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

```diff: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
```

https://3v4l.org/DPKFM/vld

PHPという言語はnamespaceに関数を定義できます。ということは、`namespace hoge;`のファイルで`strlen()`という呼び出しをすると`hoge\strlen()`という関数を呼び出そうとしているのか、トップレベルの `strlen()` を呼び出そうとしているのか判別がつきません。

`INIT_NS_FCALL_BY_NAME 'hoge%5Cvar_dump'` はnamespace内で、このように2段階で関数を検索して実行する命令です。厳格に対応がチェックされるクラスとは違って関数は細かいことを気にせずに呼び出しができるのはメリットですが、簡単に最適化のメリットを失なわせることもできる。

このようなnamespaceの呪いに対抗する方法は二つあります。

```diff:対抗策1:use function
 <?php
 namespace hoge;
+use function strlen;
+use function var_dump;
 var_dump(strlen('abc') + strlen('def'));
```

```diff:対抗策2:関数呼び出しに \ を付ける
 <?php
 namespace hoge;
-var_dump(strlen('abc') + strlen('def'));
+\var_dump(\strlen('abc') + \strlen('def'));
```

これで `INIT_NS_FCALL_BY_NAME` を避けて `INIT_FCALL` に戻すことができるのです。

:::note warn
ここで朗報です。`INIT_NS_FCALL_BY_NAME`はみなさんも無意識に日常的に起こしている非効率なコードだと思いますが、これがアプリケーションのボトルネックになることは**ほとんどない**ので、これまで書いてきたコードを思い出して不安がることはありません。
:::

:::note alert
パフォーマンスチューニングを目的に手作業で`\`を付けて回る作業は、一般的なWebアプリケーションにおける最適化の手段としては筋がよいとは言えません。暇なら止めはしませんが…
:::

:::note
一方で非効率なコードを敢えて実行するメリットもないので、新規コードで `use function` を付けるようにしていくのは良い習慣かもしれません。また、機械的に検査したり自動で付与するのもよいでしょう。
:::

## つづく

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

https://qiita.com/tadsan/items/d11229761504c8a1ca81