複数のデリミタによるexplodeは何が速いか

# 結論

* PHP7以降は`preg_split`が高速化。優位に。
* PHP5.6以前は`mb_split`が高速。PHP7での大きな変動は見られない。
* 置換を使った`explode`はどのバージョンでも比較的安定したよい速度が出る。

# 前書き

どうも。xdebugで調査するとデータベース操作とフレームワークの起動が支配的で小手先の速度改善が虚しくなってきたものです。

投稿時期を待っている間にタイムリーなコメント
[PHPで少しでも速いコードを書きたい時のためのメモ - コメント - Qiita](https://qiita.com/stk2k/items/68eeb22d5e69eeecf13d#comment-ec02523b3cde0f3a7ef3)
がありましたでの置いておきます。

この手の話はまっとうですし、折りに触れ目につきます。
しかしms単位であっても速い処理を探すのは単純に楽しい…[^2]
『納得』は全てに優先するぜッ！！と言う言葉もあるので難しいのですが、のめり込み過ぎず、やり過ぎず。

そして素人のたいていのベンチマークは何かしら不備があるものなので。
私のこの記事自体もやはり「納得」「選択」のための一資料としてお付き合いするべきだと思いました。

# 概要

`explode`関数はデリミタをひとつしか受け付けない。
より柔軟な分割関数には`preg_split`と`mb_split`があるが、どちらも正規表現を使う。

確かに「AあるいはBをデリミタとする」は正規表現の範疇だが、一般的に正規表現は遅いと言われる。

一方、`explode`の[ユーザーノートにはデリミタを配列で受け取れる関数の作り方が紹介されている。](http://php.net/manual/ja/function.explode.php#111307) [^1]

複雑な正規表現の力を必要としない単純なデリミタ、単純な対象Stringの場合、どれが速いか検証してみた。

合わせて、古くから受け継がれて使われている再帰式`explode`の性能も調査した。

# 調査環境

デリミタ、分割対象は`multiexplode`で使われていた

```php
array(",",".","|",":")
"here is a sample: this text, and this will be exploded. this also | this one too :)"
```

を使用。

コードを動かす場所として
[3v4l](https://3v4l.org/) thx [3v4lでPHPの動作検証 - Qiita](https://qiita.com/jkr_2255/items/1b132b59ab0e6b3a3a12)
で複数バージョンをチェック。

実行時間の計測には
[lavoiesl/php-benchmark: Tool to compare different functions in PHP](https://github.com/lavoiesl/php-benchmark)
を使用。
各クラスファイルをコピペで3v4lに貼り付けて動かした。

# テストコード

```php

/*
php-benchmarkのクラス記述省略
*/

$benchmark = new Benchmark;

function multiexplode ($delimiters,$string)
{
    $ready = str_replace($delimiters, $delimiters[0], $string);
    $launch = explode($delimiters[0], $ready);
    return  $launch;
}

function homemadeexplode($splitter, $str)
{
    $return = [];
    if (is_array($splitter) == true) {
        foreach ($splitter as $sp) {
            $str = homemadeexplode($sp, $str);
        }
        $return = $str;
    } else {
        if (is_array($str) == true) {
            foreach ($str as $st) {
                $tmp = explode($splitter, $st);
                $return = array_merge($return, $tmp);
            }
        } else {
            $return = explode($splitter, $str);
        }
    }
    return $return;
}


$text = "here is a sample: this text, and this will be exploded. this also | this one too :)";

$benchmark->add('multiexplode',    function() use($text) { return multiexplode(   [",",".","|",":"], $text); });
$benchmark->add('homemadeExplode', function() use($text) { return homemadeexplode([",",".","|",":"], $text); });
$benchmark->add('preg_split',      function() use($text) { return preg_split(     '/[,.|:]/',        $text); });
$benchmark->add('mb_split',        function() use($text) { return mb_split(       '[,.|:]',          $text); });

$benchmark->setCount(50000);
$benchmark->run();

```

# 結果

https://3v4l.org/s6vfi#output

![2017-12-07_13h27_09-min.png](https://qiita-image-store.s3.amazonaws.com/0/62354/a21e4bad-058b-b49b-2a1b-15a252d0dd0f.png)

![2017-12-07_13h28_26-min.png](https://qiita-image-store.s3.amazonaws.com/0/62354/fadbaeb0-e837-6814-0553-d9e6c6fbb4d7.png)

`mb_split`は稀に速度が低下し、今回の最新7.2は悪い方が出てしまったようだ。
5.6までは`preg_split`は`mb_split`と比べて明らかに遅かったが、7.0から高速化し逆転。
`multiexplode`はほぼ安定して2位を維持している。

テキストにマルチバイトを含んでも変わらないもよう。
https://3v4l.org/4Cec9#output


# 追加:デリミタが一つの場合は？

## コード

通常のexplodeを追加した。

```php
$text = "here is a sample: this text, and this will be exploded. this also | this one too :)";

$benchmark->add('multiexplode',    function() use($text) { return multiexplode(   [","], $text); });
$benchmark->add('homemadeExplode', function() use($text) { return homemadeexplode([","], $text); });
$benchmark->add('preg_split',      function() use($text) { return preg_split(     '/,/', $text); });
$benchmark->add('mb_split',        function() use($text) { return mb_split(       ',',   $text); });
$benchmark->add('explode',         function() use($text) { return explode(        ',',   $text); });

$benchmark->setCount(50000);
$benchmark->run();
```

## 結果

https://3v4l.org/XbtX1#output

![2017-12-07_14h05_57-min.png](https://qiita-image-store.s3.amazonaws.com/0/62354/ae14f6f6-c059-af38-fc1a-00e9577d638f.png)

![2017-12-07_14h06_29-min.png](https://qiita-image-store.s3.amazonaws.com/0/62354/34d75e4d-0ce8-4129-a336-5faabc3387d2.png)

えーと、何がしたかったのか自分でもわからない。
`explode`が最速なことは当然として、`preg_split`が全バージョンで比較的速い。
`explode`より速いものがあったのは流石に誤差として、
そもそも`explode`が一桁ms～二桁msの大きめなゆらぎがあることが気になる。

# あると良かった検証

デリミタなしのときの`str_split`含めた検証

# multiexplodeの注意点？

'multiexplode'はデリミタがstringの一文字でもなんとか動くが、これが複数文字やマルチバイトの場合、`$delimiters[0]`で置換してしまうので、一つでも配列に入れておこう。…タイプヒンティング

# まとめ？

今回~~は~~も多用な値でテストをしていないので純粋に信じてはいけない。
また、鬼車とPCREも置いておき。

カリカリせず、PHP7なら何も考えずに全て`preg_split`でいいかもしれない。
一文字の正規表現パターンが気持ち悪いなら単純に`explode`と`preg_split`の併用でも良い。

5.6以前なら`explode`と`mb_split`の併用がわかりやすいか。

* PHPバージョン毎の(性能改善)差異に振り回されなくない
* 複数の単純なデリミタが頻発する(正規表現を使わない)
* 性能がいい`*_split`と既に使っている正規表現関数(`preg_match`, `mb_ereg_replace`など)が逆になってしまう

なんてときは`multiexplode`も視野に入るのでは。

性能を気にするなら当然できるところは`explode`で行う。

**なお、個人的には`preg_split`で使える`$flag`引数にも注目しておきたい。(とくに`PREG_SPLIT_NO_EMPTY`)**


`explode`に限りませんが、
この場合はどっちでもいけるけどこっち～
と何らかの指針が無いと、そのとき見つけたコードで実装してしまい全体の統一感が失われることもあるので、注意したいですね。速度もですが。
~~explodeとsplitですでに統一感が無い~~

ともあれ再帰関数は不要でしょう。

# 参考

* [PHP: explode - Manual](http://php.net/manual/ja/function.explode.php)
* [PHP: preg_split - Manual](http://php.net/manual/ja/function.preg-split.php)
* [PHP: mb_split - Manual](http://php.net/manual/ja/function.mb-split.php)
* [php - preg_split vs mb_split - Stack Overflow](https://stackoverflow.com/questions/36115796/preg-split-vs-mb-split)

***

`PREG_SPLIT_NO_EMPTY`良さそう

* [explode()の挙動がキモい - Qiita]
* [【PHP】空文字のexplode()は空配列を返さない - Qiita]
* [文字列配列から空文字を除去するバッドノウハウ - Qiita]

[文字列配列から空文字を除去するバッドノウハウ - Qiita]:https://qiita.com/tadsan/items/9a0ef5b4375a3701d73d

[【PHP】空文字のexplode()は空配列を返さない - Qiita]:https://qiita.com/ma_me/items/7a131bf21d4eb1feee6d

[explode()の挙動がキモい - Qiita]:https://qiita.com/Hiraku/items/a58b8aac3f9e55cd1bd5

[^1]: かなり目からウロコ。
[^2]: ゆえに「パフォーマンス改善が目的の仕事」なら自制しなければならない