はじめに
前回に引き続き、PHP最適化Tipsについて検証していきます。
今回は文字列置換関数の比較です。またgdbを用いたPHPコードの読み方についても紹介します。
strtr > str_replace > preg_replace の順に速い
この3つの関数は細かな動きに違いはあるものの、文字列を置き換える関数です。このように同じ動きをする関数が多く存在するのは良くも悪くもPHPの特徴であるといえます。
下記のベンチマーク用のコードを用意して、計測を行います。
実行結果は上記のようになりました。
strtrとstr_replaceはほぼ同じくらいの速さ、preg_replaceは明らかに遅い結果になりました。
strtrとstr_replaceについて、性質上どのような違いがあるのか解説したいと思います。
strtrはext/standard/string.cのPHP_FUNCTION(strtr)で定義されており、php_strtrで文字列の置き換えが行われています。
PHP_FUNCTION(strtr)では、引数のvalidationと与えられた引数が配列かどうかによって置換する関数を振り分けます。
引数に文字列を与えた場合は、2833行目のphp_strtrが実行されます。
php_strtrでは、実際の文字列の置換処理が行われます。
引数trlenには2837行目 MIN(Z_STRLEN_PP(from), Z_STRLEN_PP(to)) から置き換え前後の文字列の最短の長さが渡ります。2681行目では、trlenまでのforループなため短い方にあわせて処理されます。2681行目でfromからtoへの変換する文字列のマッピング情報が一文字ずつ登録されます。長さはtrlenまでとなり置き換え前後の文字列長の短い方にあわせて置換され、置き換え前後の文字列は同じ長さしか処理されないこともわかります。
そして、2685行目のループで一文字づつ処理されていき、変換対象の文字であれば先ほどxlatに代入した文字へ置き換わります。
次にstr_replaceについて見ていきます。
str_replaceはext/standard/string.c内に記述されており、PHP_FUNCTION(str_replace) → php_str_replace_common → php_str_replace_in_subject → php_str_to_str_ex と処理されます。
順に説明していきます。
PHP_FUNCTION(str_replace) ではphp_str_replace_commonが呼ばれるだけです。
php_str_replace_common()では引数のvalidation、処理を行う文字列の配列チェックが行われます。
php_str_replace_in_subject()では置換文字列が配列で渡されているかによって処理が振り分けられ、置換処理を行う関数を実行します。
php_str_to_str_ex()では実際の置換処理が行われます。
引数に文字列が与えられた処理では、php_memnstrで置き換え対象の文字列の出現する場所を見つけ、memcpyによって文字列を置き換えます。この動作が繰り返し続けられます。
またstr_replaceでは大文字小文字を区別せずに置き換えることができるため、その際には置き換え文字列それぞれがphp_strtolowerで小文字へ変換されたり、置き換え前後の文字列の長さが違う場合には差分の文字列領域を確保するなどの処理が伴います。
置き換え後の文字列の長さを変えて、計測してみましょう。
'abc' を 'ABCDEF' と、先ほどのベンチマークから置き換え後の文字列の長さを3文字増やして再度計測を行ってみます。
処理が増えたためにわずかに遅い結果となりました。
このようにstr_replaceではパラメータになにを渡すかにより、結果は変わってきます。
ただし、strtrは検索文字と置き換え文字の一方が長い場合は長い部分は無視され、同じ長さの部分のみ処理される特徴があることを考えると、今回の検証では、正規表現が必要なとき以外はpreg_replaceは使わずに、str_replaceを使ったほうがいいと言えます。
gdbを使ってPHPのコードを読む
検証に役立つ方法として、gdbを使ってPHPのコードを読む方法をご紹介します。例として、gdbを使ってstrtrの動きを追ってみたいと思います。
ブレークポイントを設定してみましょう。あたりまえですがstrtrでは定義されてません。
strtrは PHP_FUNCTION(strtr) で定義されています。このPHP_FUNCTIONはマクロ定義されており、ヘッダファイルから定義されてるところを探すと、以下のコードがでてきます。
最終的に“zif_##name”となっているので、zif_strtrとして内部で扱っていることがわかりました。zif_strtrでbreakpointを設定すればPHPのstrtr()にbreakpointを設定することができます。
ソースファイルと行数が分かっていれば下記のように指定することも可能です。
また、クラスメソッドとして定義されてるPHP_METHOD()については、zim_(classname)_(name)で設定できます。例えば、PDOクラスのprepareメソッドはzim_PDO_prepareとして扱われます。
次に実際の処理を行っているphp_strtr()にもbreakpointを設定します。
先ほど使ったstrtrのベンチマーク用のプログラムを走らせてみます。
期待通りにzif_strtrで処理が中断されました。
再開させます。
php_strtrで処理が中断されました。与えられる引数の中身を確認できます。
php_strtrの処理終了まで進めます。
return_valueの中身をprintします。
さらに見やすく出力するためにzval出力用のprintzvを使ってみます。
printzvはphpのソースを展開すると出てくる.gdbinitに定義されています。
.gdbinitをホームへコピーすることで使用できるようになります。
無事に置換後の文字列が確認できました。
まとめ
今回はstrtr(), str_replace(), preg_replace()といった文字列置換関数の検証とgdbの解説を行いました。
普段何気なく使っている関数も、gdbなどを使って、関数の中で何が起きているのかを調べてみると新しい発見があり楽しいと思います。