PHPの文字マッチ性能比較

(Last Updated On: 2018年8月13日)

バリデーションコードを書いていると文字にマッチするパターンは結構多いです。簡単なベンチマークコードで性能を比較してみました。

文字マッチ性能比較

入力文字列が英数字+”_”+” “(スペース)のみで構成されているかチェックする関数を作って比較します。このチェックの場合、長過ぎる入力以外のリスクはほぼ廃除1できます。参考:ほぼ全てのインジェクション攻撃を無効化/防止する入力バリデーション

<?php
$str = "Finds the length of the initial segment of a string consisting entirely of characters contained within a given mask";
$loop = 100000;

// strspn
$funcs['strspn'] = function($str) {
    // 本来、ベンチマーク用にはstrlen()との比較は要らないが追加
    // 実際にこのような比較をする場合、DbCでデータ型を保証するか、$strの型チェックが必要
    return strlen($str) === strspn($str, " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz");
};

// mb_ereg
$funcs['mb_ereg'] = function ($str) {
    // \\A, \\zと書くのを面倒がって^,$を使っていたのを変更
    return mb_ereg('\\A[ _0-9a-zA-Z]+\\z', $str);
};

// preg_match
$funcs['preg_match'] = function ($str) {
    return preg_match('/\\A^[ _0-9a-zA-Z]+\\z/', $str);
};

// cached in local stack array
$funcs['cached'] = function ($str) {
        static $c = array();
        if (!isset($c[$str])) {
          $c[$str] = preg_match('/\\A[ _0-9a-zA-Z]+\\z/', $str);
          return $c[$str];
        }
        return $c[$str];
};


foreach($funcs as $k => $f) {
        $s = microtime(true);
        $l = $loop;
        while($l--) {
                $f($str);
        }
        echo "$k:   \t". (microtime(true)-$s) ."\n";
}

実行結果:

$ php -v
PHP 7.1.18-dev (cli) (built: May 24 2018 11:03:47) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.1.18-dev, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans

$ php -d xdebug.default_enable=0 t.php
strspn:     0.55522298812866
mb_ereg:    0.39360499382019
preg_match:     0.12864804267883
cached:     0.089614868164062

このケースの場合、preg_match()が圧倒的に速いです。preg_match()は正規表現をコンパイルしたデータをキャッシュしています。繰り返し実行する場合、mb_ereg()よりも良い性能を期待できます。必ずしもpreg_match()の方が圧倒的に速い訳ではありませんので注意してください。

strspn()が遅いのは文字マッチアルゴリズムの問題です。これは簡単な最適化パッチで高速化できるので、時間があったら作って適用しておきます。

何度もマッチさせるパターンがある場合、結果をキャッシュすると高速化できます。キャッシュと言っても単純にローカルstatic配列変数に結果を保存するだけです。

同じ変数を何度もバリデーションしたり、エスケープしたりするコードの実行速度オーバーヘッドが気になる場合、キャッシュした結果を単純に返すコードにすると良いでしょう。2

結果をキャッシュしてしまえば、遅くて重い複雑なバリデーションでもエスケープでも、使いたい放題です。

PHP 7.2からより高速なパラメーターパースAPIがstrspnにも利用されています。これで計測した結果は以下の通りです。

$ php -d xdebug.disable=1 -v
PHP 7.2.7-dev (cli) (built: May 24 2018 11:09:51) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.7-dev, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans

$ php -d xdebug.default_enable=0 t.php
strspn:     0.53723001480103
mb_ereg:    0.43371605873108
preg_match:     0.14471697807312
cached:     0.093671083450317

※ コメントに書いていますが、バグがあるとコメントがあったのでコードを見るとstrspn()しか呼んでいませんでした。後で思い出したのですが、これはベンチマーク結果が正しく比較できるように敢えてstrspn()しか呼ばないでおいたことを思い出しました。コメントに書いておけば良かったですね。自分でも忘れていました。ベンチマーク用のコードでも、マネをさせると困るので少しマシにしましたが、まだ実用コードとしては不十分です。更新後のコードには本来不必要なstrlen()を入れて比較していますが、ベンチマーク結果としては揺れ程度の違いしか無いようです。

メモリ利用

1つのリクエストの処理で仕様する入力値/出力値の数はそれほど多くないので気にする必要があるケースは少ないです。とは言っても入力値や出力値などの変数をキャッシュした場合にどのくらいのメモリが必要になるのか?知っておくと良いです。

<?php
$str = "Finds the length of the initial segment of a string consisting entirely of characters contained within a given mask";
$loop = 100000;

var_dump(memory_get_usage());
for($i=0; $i < $loop; $i++) {
  $a[$i][$str] = null;
}

var_dump(memory_get_usage());
for($i=0; $i < $loop; $i++) {
  $b[$i] = $str;
} 

var_dump(memory_get_usage());

実行結果

int(363976)
int(42162488)
int(46360968)

最初のループには多くのメモリが必要であったことが分ります。この結果から

  • ハッシュキーにした場合、PHP変数のCopy on Writeは機能しない

ことが判ります。

これは、PHPが内部的にはハッシュキーをPHP変数(zval)としてして扱っていないことが理由です。

最初のループはハッシュキーに文字列がコピーされて保存されています。このため、同じ文字列が大量にコピーされメモリ使用量が大幅に増えています。

後のループではハッシュに変数を代入しているのでPHP変数のCopy on Write機能が通常どおり動作し、メモリ利用量はそれほど増えていません。

スタティック変数を利用した処理結果キャッシュはそれなりにメモリが必要であることに注意しましょう。3


  1. 半角スペースを許可していることがリスクを大きく増加させています。半角スペースはトークンの区切り文字であることが多いです。トークンは命令やパラメーターに成り得ます。 
  2. キャッシュしてメモリを余計に消費するので、メモリ効率は悪くなります。トレードオフです。 
  3. メモリ利用量を減らす為、キーではなく値にキャッシュするデータを保存してはなりません。in_array()での検索はO(n^2)なのでキャッシュする意味が無くなります。ハッシュキーでの検索はO(1)です。 

投稿者: yohgaki