Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

サイズの大きな配列のループ処理部で「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: 単純に速度だけの比較なら こちらこちら のコメントも参考にしてみてください。

実験方法

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

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

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);

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

1. foreach

1.1. 通常パターン

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

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

1.2. メモリ節約パターン

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

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

1.3. 通常パターン(参照渡し)

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

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

1.4. メモリ節約パターン(参照渡し)

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

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

2. while

2.1. 通常パターン

each を利用して、先の foreach と機能的に等価な処理を書いてみました。
参考 : PHP: foreach - Manual

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

2.2. メモリ節約パターン

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

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

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

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

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

3. for

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

3.1. 通常パターン

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

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

3.2. メモリ節約パターン

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

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 を使ってみました。

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 点まとめておきます。

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

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

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

while 文に関しては、「プログラミングPHP 第3版」でも以下のように言及されています。

この方法は、foreachのように配列のコピーを作成しません。これは、大きな配列などでメモリを節約したい場合に便利です。

プログラミングPHP 第3版」より

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

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

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

h13
ハローワールド
bengo4
「専門家をもっと身近に」を理念として、人々と専門家をつなぐポータルサイト「弁護士ドットコム」「弁護士ドットコムニュース」「税理士ドットコム」を提供。
https://corporate.bengo4.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away