PHP で配列を走査して処理するのは、for / foreach だけじゃないらしいのでやってみた

#はじめに
どうも、最初にみたforeachの便利さに触れて以降、なかなかそれから離れる事ができないforeachおじさんです。

Shin x blogさんのblog
http://www.1x1.jp/blog/2014/06/how-to-scan-array-in-php.html
にて、いろいろとモダンな感じのarrayの処理方法を紹介されていたのでベンチをとってみました。

# やりたい（かった）こと
* 対象の配列が巨大な場合と、小さい場合それぞれにおけるベンチマークをとりたい
* ただし、あまり高速過ぎるとよくわからない結果になるので、配列が小さい場合にはループ回数を増やして最終的な配列の処理レコード数を同じにする
* composer使ってゴニョゴニョの練習
* しかしながら、Laravel(Illuminate\Supportパッケージ）のインストールができなかったのでこちらに関してはSkip

#試験用ソースコード

```php:array_filter_test.php
<?php
/*
http://www.1x1.jp/blog/2014/06/how-to-scan-array-in-php.html

配列内に連想配列が格納されており、nameとyearというキーを持つ
yearが、2000以上の要素のみ、結果配列に格納する
結果配列には、nameとyearを連結した文字列を格納する
*/
require_once './vendor/autoload.php';

define('ARRAY_SIZE', 100);
define('TOTAL_ROOP_COUNT', 1000000);

echo "各操作において、トータルのループ回数が".ROOP_COUNT."回になるように設定しています。\n\n";

$roop_count = TOTAL_ROOP_COUNT/ARRAY_SIZE;

$start_time=microtime(true);


for($i=0; $i< $roop_count; $i++){
    $target_arr = array();
    for($j=0; $j<ARRAY_SIZE; $j++){
        $target_arr[] = array(
            'id' => $j,
            'year' => 1914 + $j%100,
            'name' => 'Name'.$j,
        );
    }
}
echo "array size=".ARRAY_SIZE." の準備にかかった時間\n    ".(microtime(true)-$start_time)."\n\n";

$start_time = microtime(true);
for($i=0; $i< $roop_count; $i++){
    $filterd_arr = getByForeach($target_arr);
}
echo "foreachの実行にかかった時間\n    ".(microtime(true)-$start_time)."\n\n";

$start_time = microtime(true);
for($i=0; $i< $roop_count; $i++){
    $filterd_arr = getByArrayFunc1($target_arr);
}
echo "array_filter + array_mapの実行にかかった時間1(\$tmpを利用する)\n    ".(microtime(true)-$start_time)."\n\n";

$start_time = microtime(true);
for($i=0; $i< $roop_count; $i++){
    $filterd_arr = getByArrayFunc2($target_arr);
}
echo "array_filter + array_mapの実行にかかった時間2(\$tmpを利用しない)\n    ".(microtime(true)-$start_time)."\n\n";

$start_time = microtime(true);
for($i=0; $i< $roop_count; $i++){
    $filterd_arr = getByGinq($target_arr);
}
echo "ginqの実行にかかった時間\n    ".(microtime(true)-$start_time)."\n\n";

function getByForeach($array){
    $result = array();
    foreach ($array as $v) {
        if ($v['year'] <  2000) {
            continue;
        }
        $result[] = $v['year'] . $v['name'];
    }
    return $result;
}

function getByArrayFunc1($array){
    $tmp = array_filter($array, function($v) {
        return $v['year'] >= 2000;
    });
     
    $result = array_map(function($v) {
        return $v['year'] . $v['name'] ;
    }, $tmp);
    return $result;
}
function getByArrayFunc2($array){
    return array_map(
        function($v) {
            return $v['year'] . $v['name'] ;
        },
        array_filter(
            $array, 
            function($v) {
                return $v['year'] >= 2000;
            }
        )
    );
}

function getByGinq($array){
    return Ginq::from($array)->filter(function ($v) {
        return $v['year'] >= 2000;      // filter
    })->map(function ($v) {
        return $v['year'] . $v['name']; // map
    })->toArray();
}
```

# スクリプト中でやっていること
* 適当なサイズのarrayを作成
* foreachループでの実行速度を計測
* array_filter()とarray_map()での実行速度を計測
* array_filter()とarray_map()を利用するが、途中で一時変数に代入しない方法で実行速度を計測
* Ginqを使った実行速度を計測

# 実行結果
全て手元のWindowsマシン Core i5-2500@3.30GHzで測定

各操作において、array()のサイズx実行回数の(Totalの走査件数)が1,000,000回で同じなるように設定しています。

|array()のサイズ|100|1,000|10,000|100,000|
|:-----------|------------:|------------:|------------:|------------:|
|array()の準備|0.544s|0.832s|1.174s|1.324s|
|foreach|0.159s|0.160s|0.683s|0.961s|
|array_*関数(一時変数使用)|0.324s|0.295s|0.603s|1.342s|
|array_*関数(一時変数不使用)|0.309s|0.289s|0.586s|1.060s|
|Laravel|N/A|N/A|N/A|N/A|
|ginq|1.99s|1.848s|2.299s|2.522s|

# 結果の検証
* arrayが小さい時は、Foreachでの処理がかなり速いが、巨大化するとarray_*関数での処理とほぼ速度差が無くなる
* ginqは全体的に遅いが、array()サイズが大きくなった場合でも処理速度の低下が比較的小さい
* そもそも、array()の準備においてもarray()の巨大化に伴って処理速度は低下する（巨大なtextを扱おうとして急激な速度低下が発生したことは有りましたが、array()においてここまでの速度低下となるとは知りませんでした。）

# 感想
* インストール方法を記載して戴いたにもかかわらずLaravel(Illuminate\Supportパッケージ）をインストールして使うことができなかった俺カッコ悪い。
* array_filter() array_map()による無名関数の利用はなんか他のかっこいい言語っぽいので、そこまでシビアな場所でない場合は可読性と相談しながら使ってきたい。
* ginqは環境が整っていない場合においてあえて入れてまでこの機能を使いたいかどうかは微妙な感じかな。
* あと、ぶっちゃけると、速度を重視する場合においてはそもそもデータを配列に入れるためのループというものが存在する可能性が高いので、その中で同時にフィルタリングをするべきかなと思います。