Eloquent ORMのChunkとCursorをメモリ使用量で比較した

前回書いた記事で、Eloquent ORMにおけるChunkとCursorメソッドの挙動について、発行されるSQL文の観点から調べました。

blog.zuckey17.org

そこで、まとめでも書きましたが、主にメモリ使用量を抑えるために使われるChunkとCursorのメソッドについて、実際にメモリ使用量を調べてみました。

メモリ使用量について、実際に計測してみようと思った

※ 僕自身、PHPのコードでメモリ使用量を意識したことはそこまでなく*1、どのように書けば特定の処理のメモリを調べることができるのか、というところから調べたのでその辺についても少し記載しています。

※ 本エントリで利用しているコードはすべて

github.com

にあります。

また、

  • PHP7.1
  • SQLite3

という環境で実行しています。

目次

PHPの最大メモリを変更する

100,000レコードものテーブルを全件取得しようとすると

PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 4096 bytes)

と言って怒られてしまいました。 まず、手元のmacPHPの設定を見にいきます。

$ php -i | grep php.ini
Configuration File (php.ini) Path => /usr/local/etc/php/7.1
Loaded Configuration File => /usr/local/etc/php/7.1/php.ini

$ cat /usr/local/etc/php/7.1/php.ini | grep memory_limit
memory_limit = 128M

ということで128Mということがわかりました。

常に変更するのは避けたいので、ファイルのはじめに

ini_set('memory_limit', '256M');

と変更を加えておきました。

データセットを用意

歩行者移動支援サービスに関するデータサイト

こちらを利用して、約100,000件のレコードを用意しました。

php-sandbox/migration.php at master · zuckeyM-17/php-sandbox · GitHub

メモリ使用量を測定するスクリプト

php-sandbox/check.php at master · zuckeyM-17/php-sandbox · GitHub

php-sandbox/check-memory-usage/src/scripts at master · zuckeyM-17/php-sandbox · GitHub

<?php

$capsule->getConnection()->enableQueryLog();

echo "CHECK MEMORY USAGE\n";
echo "==================\n";
$start = microtime(true);

// 処理

$time = microtime(true) - $start;
$memory = memory_get_peak_usage(true) / 1024 / 1024;
echo "\n==================\n";
printf("time: %f memory: %f MB" . "\n", $time, $memory);

$queryLog = $capsule->getConnection()->getQueryLog();
foreach ($queryLog as $i => $query) {
    echo 'Query' . ($i + 1) . ': ' . $query['query'] . PHP_EOL;
}

このようなスクリプトを書いて、処理にかかった時間、最大メモリ使用量、発行したクエリを出力しました。

結果

結果は以下のようになりました。

$ php check.php all
CHECK MEMORY USAGE
==================
縄文の宿まんてんル華耀亭や亭ザ・サンプラザート
==================
time: 1.418852 memory: 172.007812 MB
Query1: select * from "inns"
$ php check.php chunk10
CHECK MEMORY USAGE
==================
縄文の宿まんてんル華耀亭や亭ザ・サンプラザート
==================
time: 59.299147 memory: 10.000000 MB
Query1: select * from "inns" order by "inns"."id" asc limit 10 offset 0
Query2: select * from "inns" order by "inns"."id" asc limit 10 offset 10
...
Query9780: select * from "inns" order by "inns"."id" asc limit 10 offset 97790
Query9781: select * from "inns" order by "inns"."id" asc limit 10 offset 97800
$ php check.php chunk100
CHECK MEMORY USAGE
==================
縄文の宿まんてんル華耀亭や亭ザ・サンプラザート
==================
time: 9.412318 memory: 4.000000 MB
Query1: select * from "inns" order by "inns"."id" asc limit 100 offset 0
Query2: select * from "inns" order by "inns"."id" asc limit 100 offset 100
~~~~~
Query978: select * from "inns" order by "inns"."id" asc limit 100 offset 97700
Query979: select * from "inns" order by "inns"."id" asc limit 100 offset 97800
$ php check.php chunk1000
CHECK MEMORY USAGE
==================
縄文の宿まんてんル華耀亭や亭ザ・サンプラザート
==================
time: 1.720385 memory: 6.000000 MB
Query1: select * from "inns" order by "inns"."id" asc limit 1000 offset 0
Query2: select * from "inns" order by "inns"."id" asc limit 1000 offset 1000
~~~~
Query97: select * from "inns" order by "inns"."id" asc limit 1000 offset 96000
Query98: select * from "inns" order by "inns"."id" asc limit 1000 offset 97000
$ php check.php cursor
CHECK MEMORY USAGE
==================
縄文の宿まんてんル華耀亭や亭ザ・サンプラザート
==================
time: 1.620625 memory: 4.000000 MB
Query1: select * from "inns"
処理 経過時間(s) 最大メモリ使用量(MB) クエリ数
all 1.418852 172.007812 1
chunk、10件ごと 59.299147 10.000000 97790
chunk、100件ごと 9.412318 4.000000 979
chunk、1000件ごと 1.720385 6.000000 98
cursor 1.620625 6.000000 1

まとめ

前回のブログで、

1回のクエリでアクセスしている分cursorのほうが実行速度は速いものの、メモリの使用量を抑えるという意味では、あまりにも膨大な量のレコードを処理する場合にはcursorよりもchunkのほうが有用なのではないかと考えられると思います。

と書きました。 しかしながら、上記の表を見る限り、Chunk1000以上にするとCursorとでは速度、メモリ使用量でそれほどの違いがなかったという結果になりました。

また、実行速度はさておき、Chunk1000とCursorとでは、

  • 1回のループで作成するモデルインスタンスの数が違う
  • 1回のクエリで取得するレコードの数が違う

にも関わらず、メモリ使用量が同じ値になったのは疑問が残りました。 Cursorの仕組みについて、Eloquentの実装方法について調べて行く必要がありそうに感じました。

検証方法や考察について間違いなどがあればどんどん指摘して欲しいです!! また、こういうこと検証、試したら面白い、調べて欲しいなどがあればTwitterやコメントなどで言っていただければ嬉しいです!!

*1:Circle CIでのテストがメモリのせいで失敗した時くらい