以下は sum([i for i in range(10000)]) と sum(i for i in range(10000)) の動作速度を比較するベンチマークです。
from benchmarker import Benchmarker with Benchmarker(1000, cycle=5, extra=1) as bench: sample = range(10000) @bench("list") def _(bm): for _ in bm: sum([i for i in sample]) @bench("generator") def _(bm): for _ in bm: sum(i for i in sample)
ベンチマーク結果です。
## Ranking real list 2.4163 (100.0) ******************** generator 3.1262 ( 77.3) ***************結果からは、リスト内包表記の方が高速、と言えます。
この結果だけを見ると、ジェネレーター内包表記を使う理由は見当たりません。
次に、range の値を 10000000 と先例よりもずっと大きくしてみます。
from benchmarker import Benchmarker with Benchmarker(5, cycle=5, extra=1) as bench: sample = range(10000000) @bench("list") def _(bm): for _ in bm: sum([i for i in sample]) @bench("generator") def _(bm): for _ in bm: sum(i for i in sample)
結果です。
## Ranking real generator 26.6801 (100.0) ******************** list 27.8582 ( 95.8) *******************今度はジェネレータ内包表記の方が高速になりました。
さらに、ベンチマーク実行中のメモリ使用量を計測してみます。
# リスト内包表記動作中 C:\>tasklist /fi "imagename eq python.exe" イメージ名 PID セッション名 メモリ使用量 =========== ===== ============= ============ python.exe 2772 Console 210,660 K # ジェネレータ内包表記動作中 C:\>tasklist /fi "imagename eq python.exe" イメージ名 PID セッション名 メモリ使用量 =========== ===== ============= ============ python.exe 2772 Console 14,704 K
リスト内包表記は、200MB を超えるメモリ使用量となっています。サイズが 10000000 のリストを作成するため、メモリ上に 10000000個の int を確保しているので、メモリ使用量が増大していると思われます(と同時に、処理速度が遅い原因になっている)。
他方、ジェネレータ内包表記では、メモリ使用量は極々平常です。
メモリ使用量の比較においては、ジェネレータが遅延評価で動作することの利点が強く出ています。
結果を振り返ります。
forループの回数が小さければ、リスト内包表記の方がジェネレータ内包表記よりも高速でした。
forループの回数が大きくなると、ジェネレータ内包表記の方が高速となり、かつメモリ使用量が少なくて済みました。
この結果を踏まえ、forループのサイズによってリスト内包表記かジェネレータ内包表記かを切り替える、という手法が考えられます。
が、個人的には、この手法は早過ぎる最適化になるのではないかと思います。どこまでが小さいデータでどこからが大きなデータなのか、というのは環境に依存するので一概には言えませんし、速度に気を取られてメモリ不足が起こってしまっては元も子もありません。
「ジェネレータ内包表記で書ける場面では常にジェネレータ内包表記を使う」という方式の方が、ルールとしてシンプルで良いのではないかと思います(もちろん、処理速度が問題になれば適宜対応が必要となります)。
0 件のコメント:
コメントを投稿