リスト内包表記とジェネレータの比較

エキPy読書会でジェネレータお勧め!どんどん使うべし的な話がありました。
ジェネレータの使いどころとしては、膨大なデータを扱うときや実行に時間がかかるときということでした。
リスト内包と比べるとメモリの使用量が少なくて済むことや、複雑な処理もかけるというところが優位といったところでしょうか。
逆にリスト内包では速度面でジェネレータより優位でしょう。
ということで、リスト内包がジェネレータよりどれくらい速度面で有利か実際に速度を比較してみました。

文字列'1'を大量に生成して連結するものと、
1をひたすら足していくもので比較してみました。
コードは以下のとおり

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import timeit

import gc
gc.disable()

print("JOIN SMALL LIST : ", timeit.Timer("'+'.join(['1' for x in range(0,1000)])").timeit(number=1000))
print("JOIN SMALL GEN  : ", timeit.Timer("'+'.join(('1' for x in range(0,1000)))").timeit(number=1000))
print("JOIN LARGE LIST : ", timeit.Timer("'+'.join(['1' for x in range(0,1000000)])").timeit(number=10))
print("JOIN LARGE GEN  : ", timeit.Timer("'+'.join(('1' for x in range(0,1000000)))").timeit(number=10))

print("SUM SMALL LIST : ", timeit.Timer("sum([1 for x in range(0,1000)])").timeit(number=1000))
print("SUM SMALL GEN  : ", timeit.Timer("sum((1 for x in range(0,1000)))").timeit(number=1000))
print("SUM LARGE LIST : ", timeit.Timer("sum([1 for x in range(0,1000 * 1000 * 10)])").timeit(number=10))
print("SUM LARGE GEN  : ", timeit.Timer("sum((1 for x in range(0,1000 * 1000 * 10)))").timeit(number=10))

結果は次のとおり

$ python3 time.py
JOIN SMALL LIST : 0.172473907471
JOIN SMALL GEN : 0.254935979843
JOIN LARGE LIST : 2.00245213509
JOIN LARGE GEN : 2.79136586189
SUM SMALL LIST : 0.168716192245
SUM SMALL GEN : 0.216814041138
SUM LARGE LIST : 18.0934979916
SUM LARGE GEN : 22.7823500633

joinに関してはリスト内包の方が1.4倍近く、sumに関しては1.3倍近く早いようです。
ジェネレータだとnextメソッドの呼び出しがどうしても入るのでその分が響くんだろうと予想してます。
リストのC実装は見てないのでわかりません・・・w
python3ではジェネレータの速度が早くなったと聞いていたんですが、
まだまだリスト内包には及ばない用です。
やはり状況によってうまく使い分けることが必要ですね。

    • -

以下余談ですが、
配列を同じ値で初期化するには以下のような方法もあります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import timeit

import gc
gc.disable()

print("JOIN SMALL OTHER: ", timeit.Timer("'+'.join(['1'] * 1000)").timeit(number=1000))
print("JOIN LARGE OTHER: ", timeit.Timer("'+'.join(['1'] * 1000000)").timeit(number=10))

print("SUM SMALL OTHER: ", timeit.Timer("sum([1] * 1000)").timeit(number=1000))
print("SUM LARGE OTHER: ", timeit.Timer("sum([1] * (1000 * 1000 * 10))").timeit(number=10))

これを実行すると以下のような結果になります。

$ python3 time.py
JOIN SMALL OTHER: 0.0509331226349
JOIN LARGE OTHER: 0.722629070282
SUM SMALL OTHER: 0.0472800731659
SUM LARGE OTHER: 5.57367396355

join、sumの両方ともに約3-4倍ほど高速です。
今回の用な単純なデータを生成するときにしか使えませんが、
リスト内包、ジェネレータにとらわれず色々試行錯誤することが大切ですね。

    • -

追記 2010/09/13 11:25

methaneさんより、gcをonにしたらジェネレータの方が早くならないか?というコメントをいただいたので、on-off時の速度も載せてみます。
ちなみに、gcをoffにした理由は、methaneさんもおっしゃっているように、
オブジェクトを一気に作る(メモリを消費する)とgcが動いて、
gcが介在しない「純粋な」速度比較ができないからといった理由からです。
コードは以下のとおり。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import timeit

print("GC ON")
print("JOIN SMALL LIST : ", timeit.Timer("'+'.join(['1' for x in range(0,1000)])").timeit(number=1000))
print("JOIN SMALL GEN  : ", timeit.Timer("'+'.join(('1' for x in range(0,1000)))").timeit(number=1000))
print("JOIN SMALL OTHER: ", timeit.Timer("'+'.join(['1'] * 1000)").timeit(number=1000))
print("JOIN LARGE LIST : ", timeit.Timer("'+'.join(['1' for x in range(0,1000000)])").timeit(number=10))
print("JOIN LARGE GEN  : ", timeit.Timer("'+'.join(('1' for x in range(0,1000000)))").timeit(number=10))
print("JOIN LARGE OTHER: ", timeit.Timer("'+'.join(['1'] * 1000000)").timeit(number=10))

print("SUM SMALL LIST : ", timeit.Timer("sum([1 for x in range(0,1000)])").timeit(number=1000))
print("SUM SMALL GEN  : ", timeit.Timer("sum((1 for x in range(0,1000)))").timeit(number=1000))
print("SUM SMALL OTHER: ", timeit.Timer("sum([1] * 1000)").timeit(number=1000))
print("SUM LARGE LIST : ", timeit.Timer("sum([1 for x in range(0,1000 * 1000 * 10)])").timeit(number=10))
print("SUM LARGE GEN  : ", timeit.Timer("sum((1 for x in range(0,1000 * 1000 * 10)))").timeit(number=10))
print("SUM LARGE OTHER: ", timeit.Timer("sum([1] * (1000 * 1000 * 10))").timeit(number=10))

import gc
gc.disable()

print("GC OFF")
print("JOIN SMALL LIST : ", timeit.Timer("'+'.join(['1' for x in range(0,1000)])").timeit(number=1000))
print("JOIN SMALL GEN  : ", timeit.Timer("'+'.join(('1' for x in range(0,1000)))").timeit(number=1000))
print("JOIN SMALL OTHER: ", timeit.Timer("'+'.join(['1'] * 1000)").timeit(number=1000))
print("JOIN LARGE LIST : ", timeit.Timer("'+'.join(['1' for x in range(0,1000000)])").timeit(number=10))
print("JOIN LARGE GEN  : ", timeit.Timer("'+'.join(('1' for x in range(0,1000000)))").timeit(number=10))
print("JOIN LARGE OTHER: ", timeit.Timer("'+'.join(['1'] * 1000000)").timeit(number=10))

print("SUM SMALL LIST : ", timeit.Timer("sum([1 for x in range(0,1000)])").timeit(number=1000))
print("SUM SMALL GEN  : ", timeit.Timer("sum((1 for x in range(0,1000)))").timeit(number=1000))
print("SUM SMALL OTHER: ", timeit.Timer("sum([1] * 1000)").timeit(number=1000))
print("SUM LARGE LIST : ", timeit.Timer("sum([1 for x in range(0,1000 * 1000 * 10)])").timeit(number=10))
print("SUM LARGE GEN  : ", timeit.Timer("sum((1 for x in range(0,1000 * 1000 * 10)))").timeit(number=10))
print("SUM LARGE OTHER: ", timeit.Timer("sum([1] * (1000 * 1000 * 10))").timeit(number=10))

で、結果は以下のとおり。

$ python3 time.py
GC ON
JOIN SMALL LIST : 0.180270195007
JOIN SMALL GEN : 0.263254165649
JOIN SMALL OTHER: 0.0492539405823
JOIN LARGE LIST : 1.99416089058
JOIN LARGE GEN : 2.78649997711
JOIN LARGE OTHER: 0.726522922516
SUM SMALL LIST : 0.173731803894
SUM SMALL GEN : 0.229410171509
SUM SMALL OTHER: 0.0544180870056
SUM LARGE LIST : 17.9849619865
SUM LARGE GEN : 24.5595469475
SUM LARGE OTHER: 5.52004003525
GC OFF
JOIN SMALL LIST : 0.175493001938
JOIN SMALL GEN : 0.252235889435
JOIN SMALL OTHER: 0.0482840538025
JOIN LARGE LIST : 1.97846293449
JOIN LARGE GEN : 2.78740787506
JOIN LARGE OTHER: 0.715709924698
SUM SMALL LIST : 0.183133125305
SUM SMALL GEN : 0.236751794815
SUM SMALL OTHER: 0.0463128089905
SUM LARGE LIST : 18.0269918442
SUM LARGE GEN : 24.1297588348
SUM LARGE OTHER: 5.46452307701

gc-offの時の方がgc-onの時より全体的に早くなってますが、(一部遅くなってる!?)
ジェネレータの方がやはり遅いみたいです。。。
なにか根本的に考え方(コード)が間違ってるのだろうか・・・