[PHP]ループのメモリ使用量と処理速度を比較しました

サイズの大きな配列のループ処理部で「Allowed memory size of ..」といったメモリ不足エラーの対応を行いました。
メモリと速度の効率を高めるには、どのループ文（foreach, while, for..）を選択しどう実装すべきか、かんたんに比較検証してみることにしました。
使用したパソコンのスペック、 PHP のバージョンは以下の通りです。

```
MacBook Air
OS X Yosemite 10.10.2
プロセッサ 1.7 GHz Intel Core i7
メモリ 8 GB 1600 MHz DDR3

PHP 5.5.14 (cli) (built: Sep  9 2014 19:09:25) 
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
```

:white_check_mark: 単純に速度だけの比較なら [こちら](#comment-db1087647d864f914f8c) や [こちら](#comment-3f9862a4ec81177b705e) のコメントも参考にしてみてください。

## 実験方法

まず 10 万件の配列を生成します。
これをループで回して、変数に代入します。
この処理を 100 回行い、「最大メモリ使用量との差分値」と「処理時間」をそれぞれ計測します。

以下が、サンプルプログラムです。

```php:sample.php
$baseMemoryUsage = memory_get_usage();
$baseTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
    $data = range(1, 100000);
    // ここでループ処理を行います。
    $data = null;
}

$maxMemoryUsage = (memory_get_peak_usage() - $baseMemoryUsage) / (1024 * 1024);
$processTime = microtime(true) - $baseTime;

printf("Max Memory Usage : %.3f [MB]\n", $maxMemoryUsage);
printf("Process Time : %.2f [s]\n", $processTime);
```

ループ処理は、`foreach` 、 `while` 、 `for` の計 3 種類を比較します。
サンプルプログラムをそのまま実行したもの（[NOP](http://ja.wikipedia.org/wiki/NOP)）と、ループにメモリ使用量削減効果が期待出来るような処理を組み合わせた計 10 パターンを測定しました。
※ 当初は 6 パターンほどの比較でしたが、先輩の [Koichi](http://qiita.com/Koichi) にもご指摘いただいたので興味あるパターンすべて試してみることにしました。

### 1. `foreach`

#### 1.1. 通常パターン

PHP では一番よく使うループ文なので、この計測がベースになります。

```php:testForeach.php
$output = [];
foreach ($data as $value) {
    $output[] = $value;
}
```

#### 1.2. メモリ節約パターン

1.1. のループ文から、都度利用した変数の割当を解除したパターンです。
`foreach` では配列をコピーして利用するため、メモリ節約効果は期待できないと思いますが、他との比較のために実施しました。

```php:testForeachEco.php
$output = [];
foreach ($data as $key => $value) {
    $output[] = $value;
    unset($data[$key]);
}
```

#### 1.3. 通常パターン（参照渡し）

私は業務で利用したことがないのですが、参照渡しを利用したパターンも試してみます。

```php:testForeachReference.php
$output = [];
foreach ($data as &$value) {
    $output[] = $value;
}
```

#### 1.4. メモリ節約パターン（参照渡し）

参照渡しで、変数の割当を解除してみます。
これは少し効果が期待できるのではないでしょうか。

```php:testForeachReferenceEco.php
$output = [];
foreach ($data as &$value) {
    $output[] = $value;
    unset($value);
}
```

### 2. `while`

#### 2.1. 通常パターン

`each` を利用して、先の `foreach` と機能的に等価な処理を書いてみました。
*参考 : [PHP: foreach - Manual](http://php.net/manual/ja/control-structures.foreach.php)*

```php:testWhile.php
$output = [];
while (list(, $value) = each($data)) {
    $output[] = $value;
}
```

#### 2.2. メモリ節約パターン

2.1. のループ文から、都度利用した変数の割当を解除したパターンです。

```php:testWhileEco.php
$output = [];
while (list($key, $value) = each($data)) {
    $output[] = $value;
    unset($data[$key]);
}
```

#### 2.3. メモリ節約パターン `array_shift`

`unset` ではなく、`array_shift` で代入と同時に配列内の要素を減らしていくとどうなるのでしょう。
こちらのパターンも試してみます。

```php:testWhileEcoShift.php
$output = [];
while ($value = array_shift($data)) {
    $output[] = $value;
}
```


### 3. for

※ 配列の要素数を変数に保存しておかないと、 3.2. にバグが生じると指摘を受け一部修正しました。

#### 3.1. 通常パターン

一般的な for 文です。
イテレータ操作もないし、コピーも作成しないので最もパフォーマンスがよいのではと期待しています。

```php:testFor.php
$output = [];
$count = count($data);
for ($j = 0; $j < $count; $j++) {
    $output[] = $data[$j];
}
```

#### 3.2. メモリ節約パターン

3.1. のループ文から、都度利用した変数の割当を解除したパターンです。

```php:testForEco.php
$output = [];
$count = count($data);
for ($j = 0; $j < $count; $j++) {
    $output[] = $data[$j];
    unset($data[$j]);
}
```

#### 3.3. メモリ節約パターン `array_shift`

あまり一般的な `for` 文の書き方ではありませんが、こっちでも `array_shift` を使ってみました。

```php:testForEcoShift.php
$output = [];
$count = count($data);
for ($j = 0; $j < $count; $j++) {
    $output[] = array_shift($data);
}
```

## 結果

|No.|ループ文|パターン|メモリ[MB]|処理時間[s]|
|:-:|:-:|:-:|--:|--:|
|0.0.|NOP|-|13.97|1.725|
|1.1.|foreach|通常|27.96|4.088|
|1.2.|foreach|節約|28.20|5.028|
|1.3.|foreach|通常-参照渡し|27.95|3.728|
|1.4.|foreach|節約-参照渡し|27.95|4.374|
|2.1.|while|通常|28.20|10.813|
|2.2.|while|節約|28.06|5.936|
|2.3.|while|array_shift|28.20|14460.125|
|3.1.|for|通常|27.96|4.074|
|3.2.|for|節約|28.08|2.778|
|3.3.|for|array_shift|28.20|14214.452|


## 結論

「最大メモリ使用量」では、「 **`foreach` 文の参照渡し** 」が最も効率が良いようです。
「処理速度」では、「 **`for` 文で利用した変数の割当を都度解除** 」が最も速く処理できます。

パフォーマンスを要求される処理を記述するときは、この結果を参考に実装してみてはいかがでしょうか。

...とはいえ `foreach` 文の参照渡しは、私はあまり利用しない気がします。。


## 気になったこと

以下、気になったことを 2 点まとめておきます。

### 最大メモリ使用量は、各ループ文でほとんど変わらない

「最大メモリ使用量」に関して、`unset` や `null` 代入は思っていたような削減効果を得ることが出来ませんでした。
「最大」ではなく、定期的にメモリ使用量を出力しプロットするとまた興味深いデータが得られるかもしれません。

またループ文の比較では、 `while` と `for` が同程度で優位に立つのではと予測していました。

`while` 文に関しては、「[プログラミングPHP 第3版](http://www.amazon.co.jp/dp/4873116686/)」でも以下のように言及されています。

> この方法は、foreachのように配列のコピーを作成しません。これは、大きな配列などでメモリを節約したい場合に便利です。
> 
> *「[プログラミングPHP 第3版](http://www.amazon.co.jp/dp/4873116686/)」より* 

しかし、結果を見る限りではメモリ節約効果はないように思います。
コピーは作成されないが、処理自体にメモリを消費するということでしょうか。

### 大きな配列処理に `array_shift` は向かない

もともと「PHP Fatal error: Allowed memory size of xxx」のようなエラーが発生すると思って実施しました。
しかし、結果を見ると最大メモリ使用量は他のパターンと特に変わりなく、処理自体に時間がかかるだけということが分かりました。
配列のキーの振り直しというのは、メモリには影響を与えないのですね。
この辺りも良い気づきになりました。