14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PHPAdvent Calendar 2022

Day 5

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

Last updated at Posted at 2022-12-21

こんにちは! どんどんえこひいきしていきましょう!

というわけで、後篇のこの記事では最適化の恩恵が受けられる場合において、どのような関数がその対象になるのかを見ていきましょう。

この記事では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);

https://3v4l.org/2Vkk4/vld

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);

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

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_ARGSFUNC_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発表してきました。ということで以下の記事に続く。

  1. PHPバス係数の1。参考: インフィニットループは PHP の継続的な発展を目指す The PHP Foundation に寄付をしました

14
3
0

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
  3. You can use dark theme
What you can do with signing up
14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?