徹底検証!PHP最適化Tips

第2回文字列置換関数の比較とgdbの使い

はじめに

前回に引き続き、PHP最適化Tipsについて検証していきます。

今回は文字列置換関数の比較です。またgdbを用いたPHPコードの読み方についても紹介します。

strtr > str_replace > preg_replace の順に速い

この3つの関数は細かな動きに違いはあるものの、文字列を置き換える関数です。このように同じ動きをする関数が多く存在するのは良くも悪くもPHPの特徴であるといえます。

下記のベンチマーク用のコードを用意して、計測を行います。

benchmark_strtr.php
<?php
$t = microtime(true);
$i = 0;
while($i < 1000) {
   $a = strtr('abcdefghijklmn', 'abc', 'ABC');
   ++$i;        
}
$tmp = microtime(true) - $t;
var_dump($tmp);
?>
benchmark_strreplace.php
<?php
$t = microtime(true);
$i = 0;
while ($i < 1000) {
   $a = str_replace('abc', 'ABC', 'abcdefghijklmn');
   ++$i;        
}
$tmp = microtime(true) - $t;
var_dump($tmp);
?>
benchmark_pregreplace.php
<?php
$t = microtime(true);
$i = 0;
while ($i < 1000) {
   $a = preg_replace('/abc/', 'ABC', 'abcdefghijklmn');
   ++$i;        
}
$tmp = microtime(true) - $t;
var_dump($tmp);
?>
$ php benchmark_strtr.php
float(0.000890970230103)
$ php benchmark_strreplace.php
float(0.000858068466187)
$ php benchmark_pregreplace.php
float(0.00124001502991)

実行結果は上記のようになりました。 strtrとstr_replaceはほぼ同じくらいの速さ、preg_replaceは明らかに遅い結果になりました。

strtrとstr_replaceについて、性質上どのような違いがあるのか解説したいと思います。 strtrはext/standard/string.cのPHP_FUNCTION(strtr)で定義されており、php_strtrで文字列の置き換えが行われています。

2804 PHP_FUNCTION(strtr)
      中略
2825     if (ac == 2) {
2826         php_strtr_array(return_value, Z_STRVAL_PP(str), Z_STRLEN_PP(str), HASH_OF(*from));
2827     } else {
2828         convert_to_string_ex(from);
2829         convert_to_string_ex(to);
2830 
2831         ZVAL_STRINGL(return_value, Z_STRVAL_PP(str), Z_STRLEN_PP(str), 1);
2832 
2833         php_strtr(Z_STRVAL_P(return_value),
2834                   Z_STRLEN_P(return_value),
2835                   Z_STRVAL_PP(from),
2836                   Z_STRVAL_PP(to),
2837                   MIN(Z_STRLEN_PP(from),
2838                   Z_STRLEN_PP(to)));
2839     }
2840 }

PHP_FUNCTION(strtr)では、引数のvalidationと与えられた引数が配列かどうかによって置換する関数を振り分けます。 引数に文字列を与えた場合は、2833行目のphp_strtrが実行されます。

php_strtrでは、実際の文字列の置換処理が行われます。

2670 PHPAPI char *php_strtr(char *str, int len, char *str_from, char *str_to, int trlen)
2671 {
2672     int i;
2673     unsigned char xlat[256];
2674 
2675     if ((trlen < 1) || (len < 1)) {
2676         return str;
2677     }
2678 
2679     for (i = 0; i < 256; xlat[i] = i, i++);
2680 
2681     for (i = 0; i < trlen; i++) {
2682         xlat[(unsigned char) str_from[i]] = str_to[i];
2683     }
2684 
2685     for (i = 0; i < len; i++) {
2686         str[i] = xlat[(unsigned char) str[i]];
2687     }
2688 
2689     return str;
2690 }

引数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 と処理されます。

順に説明していきます。

3770 PHP_FUNCTION(str_replace)
3771 {
3772     php_str_replace_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
3773 }

PHP_FUNCTION(str_replace) ではphp_str_replace_commonが呼ばれるだけです。

3700 static void php_str_replace_common(INTERNAL_FUNCTION_PARAMETERS, int case_sensitivity)

php_str_replace_common()では引数のvalidation、処理を行う文字列の配列チェックが行われます。

3584 static void php_str_replace_in_subject(zval *search, zval *replace, zval **subject, zval *result, int case_sensitivity, int *replace_count)

php_str_replace_in_subject()では置換文字列が配列で渡されているかによって処理が振り分けられ、置換処理を行う関数を実行します。

3403 PHPAPI char *php_str_to_str_ex(char *haystack, int length,
3404     char *needle, int needle_len, char *str, int str_len, int  *_new_length, int case_sensitivity, int *repla     ce_count)

php_str_to_str_ex()では実際の置換処理が行われます。

3417                 end = new_str + length;
3418                 for (p = new_str; (r = php_memnstr(p, needle, needle_len,  end)); p = r + needle_len) {
3419                     memcpy(r, str, str_len);
3420                     if (replace_count) {
3421                         (*replace_count)++;
3422                     }
3423                 }

引数に文字列が与えられた処理では、php_memnstrで置き換え対象の文字列の出現する場所を見つけ、memcpyによって文字列を置き換えます。この動作が繰り返し続けられます。

またstr_replaceでは大文字小文字を区別せずに置き換えることができるため、その際には置き換え文字列それぞれがphp_strtolowerで小文字へ変換されたり、置き換え前後の文字列の長さが違う場合には差分の文字列領域を確保するなどの処理が伴います。

置き換え後の文字列の長さを変えて、計測してみましょう。

'abc' を 'ABCDEF' と、先ほどのベンチマークから置き換え後の文字列の長さを3文字増やして再度計測を行ってみます。

kajidai@laputa:~$ php benchmark_strtr2.php
float(0.00085186958313)
kajidai@laputa:~$ php benchmark_strreplace2.php
float(0.000946998596191)

処理が増えたためにわずかに遅い結果となりました。

このようにstr_replaceではパラメータになにを渡すかにより、結果は変わってきます。 ただし、strtrは検索文字と置き換え文字の一方が長い場合は長い部分は無視され、同じ長さの部分のみ処理される特徴があることを考えると、今回の検証では、正規表現が必要なとき以外はpreg_replaceは使わずに、str_replaceを使ったほうがいいと言えます。

gdbを使ってPHPのコードを読む

検証に役立つ方法として、gdbを使ってPHPのコードを読む方法をご紹介します。例として、gdbを使ってstrtrの動きを追ってみたいと思います。

ブレークポイントを設定してみましょう。あたりまえですがstrtrでは定義されてません。

(gdb) b strtr
Function "strtr" not defined.

strtrは PHP_FUNCTION(strtr) で定義されています。このPHP_FUNCTIONはマクロ定義されており、ヘッダファイルから定義されてるところを探すと、以下のコードがでてきます。

main/php.h
332 /* PHP-named Zend macro wrappers */
333 #define PHP_FN                  ZEND_FN
334 #define PHP_MN                  ZEND_MN
335 #define PHP_NAMED_FUNCTION      ZEND_NAMED_FUNCTION
336 #define PHP_FUNCTION            ZEND_FUNCTION
337 #define PHP_METHOD              ZEND_METHOD
Zend/zend_API.h
43 #define ZEND_FN(name) zif_##name
44 #define ZEND_MN(name) zim_##name
45 #define ZEND_NAMED_FUNCTION(name)       void name(INTERNAL_FUNCTION_PARAMETERS)
46 #define ZEND_FUNCTION(name)             ZEND_NAMED_FUNCTION(ZEND_FN(name))
47 #define ZEND_METHOD(classname, name)    ZEND_NAMED_FUNCTION(ZEND_MN(classname##_##name))

最終的に⁠zif_##name⁠となっているので、zif_strtrとして内部で扱っていることがわかりました。zif_strtrでbreakpointを設定すればPHPのstrtr()にbreakpointを設定することができます。

(gdb) b zif_strtr
Breakpoint 1 at 0x826c000: file /home/kajidai/work/php5-5.2.6/ext/standard/string.c, line 2807.

ソースファイルと行数が分かっていれば下記のように指定することも可能です。

b stfing.c:2807

また、クラスメソッドとして定義されてるPHP_METHOD()については、zim_(classname)_(name)で設定できます。例えば、PDOクラスのprepareメソッドはzim_PDO_prepareとして扱われます。

499 static PHP_METHOD(PDO, prepare)

次に実際の処理を行っているphp_strtr()にもbreakpointを設定します。

(gdb) b php_strtr
Breakpoint 2 at 0x81fd81c: file /home/kajidai/work/php-5.2.6/ext/standard/string.c, line 2671.

先ほど使ったstrtrのベンチマーク用のプログラムを走らせてみます。

(gdb) run ~/benchmark_strtr.php
Starting program: /usr/local/bin/php ~/benchmark_strtr.php
[Thread debugging using libthread_db enabled]
[New Thread 0xb7c519e0 (LWP 25628)]
[Switching to Thread 0xb7c519e0 (LWP 25628)]

Breakpoint 1, zif_strtr (ht=3, return_value=0x8489904, return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
   at /home/kajidai/work/php-5.2.6/ext/standard/string.c:2805
2805    {

期待通りにzif_strtrで処理が中断されました。

再開させます。

(gdb) c
Continuing.

Breakpoint 2, php_strtr (str=0x848991c "abcdefghijklmn", len=14,  str_from=0x848a6ac "abc", str_to=0x848a6bc "ABC", trlen=3)
   at /home/kajidai/work/php-5.2.6/ext/standard/string.c:2671
2671    {

php_strtrで処理が中断されました。与えられる引数の中身を確認できます。

php_strtrの処理終了まで進めます。

(gdb) finish
Run till exit from #0  php_strtr (str=0x848991c "abcdefghijklmn", len=14,  str_from=0x848a6ac "abc", str_to=0x848a6bc "ABC", trlen=3)
   at /home/kajidai/work/php-5.2.6/ext/standard/string.c:2671
zif_strtr (ht=3, return_value=0x8489904, return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
   at /home/kajidai/work/php-5.2.6/ext/standard/string.c:2840
2840    }
Value returned is $1 = 0x848991c "ABCdefghijklmn"

return_valueの中身をprintします。

(gdb) p *return_value
$2 = {value = {lval = 138975516, dval = 2.9776604101582473e-313, str = {val = 0x848991c "ABCdefghijklmn", len = 14}, 
   ht = 0x848991c, obj = {handle = 138975516, handlers = 0xe}}, refcount = 1,  type = 6 '¥006', is_ref = 0 '¥0'}

さらに見やすく出力するためにzval出力用のprintzvを使ってみます。 printzvはphpのソースを展開すると出てくる.gdbinitに定義されています。 .gdbinitをホームへコピーすることで使用できるようになります。

(gdb) printzv return_value
[0x08489904] (refcount=1) string(14): "ABCdefghijklmn"

無事に置換後の文字列が確認できました。

まとめ

今回はstrtr(), str_replace(), preg_replace()といった文字列置換関数の検証とgdbの解説を行いました。

普段何気なく使っている関数も、gdbなどを使って、関数の中で何が起きているのかを調べてみると新しい発見があり楽しいと思います。

おすすめ記事

記事・ニュース一覧