文字列の先頭マッチ・末尾マッチはどうするのが最速か

  • 32
    Like
  • 5
    Comment

先頭マッチ

任意の文字列が与えられた時、先頭が"abc"であることを判定したい。
いろいろ書き方があると思うが、どう書くのが良いのか。

「文字列が含まれているか」を判定するときはstrposを使うのが最速だと知られている。
strposは最初にneedleが現れた位置を数字で返し、見つからなければfalseを返す。

if (false !== strpos($haystack, $needle)) {
  echo "$needle$haystack の中に含まれています";
}

先頭マッチの場合は、つまり見つかった位置が"0"であればいいのでこうなるのだけど、

if (0 === strpos($haystack, $needle)) {
  echo "$haystack$needle から始まります";
}

ぼんやり思ったのは、長い文字列で最後までマッチしなかった場合に遅いんじゃないかってこと。先頭だけ見ればいいのだから、切り出して比較するだけでいいはずだ。もっと早くできる。

というわけで色々考えてみた。

<?php
const LOOP = 10000;

function bench($name, callable $fn) {
    echo "$name\t";
    $start = microtime(true);
    for ($i=0; $i<LOOP; ++$i) {
        $fn();
    }
    echo microtime(true) - $start, PHP_EOL;
}

define('example', 'http://' . file_get_contents('/dev/urandom', false, null, 0, '204800'));
//define('example', file_get_contents('/dev/urandom', false, null, 0, '204800'));
//define('example', file_get_contents('/dev/urandom', false, null, 0, '204800') . 'http');


// 先頭マッチの書き方色々
bench('strpos    ', function(){
    0 === strpos(example, 'http');
});

bench('substr    ', function(){
    'http' === substr(example, 0, 4);
});

bench('preg_match', function(){
    preg_match('/^http/', example);
});

bench('substr_compare', function(){
    0 === substr_compare(example, 'http', 0, 4);
});

bench('strncmp      ', function(){
    0 === strncmp(example, 'http', 4);
});

実際に先頭に含まれている場合はこうだった。strposも速い。

strpos          0.16053009033203
substr          0.17386889457703
preg_match      0.17554497718811
substr_compare  0.16866993904114
strncmp         0.15988111495972

一方、文字列が含まれてない時。

strpos          0.33207702636719
substr          0.1786060333252
preg_match      0.16852688789368
substr_compare  0.16110396385193
strncmp         0.15551090240479

予想通りstrpos遅い。安定して速いのはstrncmp。
このためだけに存在するような関数だし、使ってあげるのがよさそう。

一番素直なのはsubstrかな。
substr_compareは文字数が足りない場合はエラーが発生してしまうので使いにくいです。
「PHP Warning: substr_compare(): The start position cannot exceed initial string length」

末尾マッチ

似たようなところで最後尾が特定の文字列で終わるかどうかのチェック。

<?php
const LOOP = 10000;

function bench($name, callable $fn) {
    echo "$name\t";
    $start = microtime(true);
    for ($i=0; $i<LOOP; ++$i) {
        $fn();
    }
    echo microtime(true) - $start, PHP_EOL;
}

//define('example', 'http://' . file_get_contents('/dev/urandom', false, null, 0, '204800'));
//define('example', file_get_contents('/dev/urandom', false, null, 0, '204800'));
define('example', file_get_contents('/dev/urandom', false, null, 0, '204800') . 'http');

// 末尾マッチ
bench('substr    ', function(){
    'http' === substr(example, -4);
});

bench('preg_match', function(){
    preg_match('/http$/', example);
});

bench('substr_compare', function(){
    0 === substr_compare(example, 'http', -4);
});
substr          0.15582203865051
preg_match      1.3190159797668
substr_compare  0.15913701057434

おや。preg_matchが遅い。正規表現って末尾だけ調べるってことができないのかな。
substrの方が速いとなると、substr_compareの存在意義が謎だなー

結論

substrで切り出して比較。が先頭でも末尾でも使えて、安定して速いのでいいと思います。