読者です 読者をやめる 読者になる 読者になる

ゆくゆくは有へと

おかゆ/オカ∃/大鹿有生/彼ノ∅有生 の雑記

ありふれたクソみたいな記事(シーケンスの速度比較)

括弧が逆ゥ!

はてブ見てたらこういうの見つけた:

Python簡単実験:内包で何倍高速になるか - TIM Labs

「なんで対話型インタプリタ使ってるんや・・・・・?」という疑問は置いといて、こんなに違うのかとちょっと信じ切れなかったので、自分でも書きました。

まずは4つ

作ったのは4つで、

  1. 単純に for で回す
  2. 内包表記使う
  3. range をそのまま list に投げる
  4. ジェネレータを list に投げる

です。

# coding=utf-8

import time


def time_calc(func, *arg, **kwds):
    start = time.time()
    func(*arg, **kwds)
    end = time.time()
    return end - start


def calc(func, n, *arg, **kwds):
    times = [time_calc(func, *arg, **kwds) for _ in range(n)]
    return sum(times) / n


def simply_for(n):
    a = list()
    for i in range(n):
        a.append(i)
    return a


def comprehension(n):
    a = [x for x in range(n)]
    return a


def range_case(n):
    a = list(range(n))
    return a


def range_(n):
    for i in range(n):
        yield i


def generator_case(n):
    a = list(range_(n))
    return a


if __name__ == '__main__':
    n = 10**7
    for func in (simply_for, comprehension,
                 range_case, generator_case):
        print("{}: {}sec.".format(func.__name__, calc(func, 5, n)))

おかゆのパソコンはうんちなのですが、とりあえずこうなりました*1

simply_for: 2.1997323036193848sec.
comprehension: 1.3124329090118407sec.
range_case: 0.550351619720459sec.
generator_case: 2.0610249996185304sec.

内包表記、うちのぱちょこんだと2倍も速くならないくらいですね。まあ速いことに変わりはありません。 それにしても、list(range(n)) が思った以上に速いですね…。

上の記事だと、単純な for より内包表記は約5倍ほど高速になってることを考えると、list(range) は10倍くらい速いことになりそう。

とはいえ、値を計算して…というのはできないですから、結局、内包表記使うのが速くていいですね。

あとは…、おかゆパソコンだと非常に微妙ですが、130msほどジェネレータつくってやるほうが速いですね。

内包表記はforが入れ子になると表記が煩わしくなるので、そういうときはジェネレータ作ってやると for よりは速いということになるのかもしれない(オーバーヘッドはあるだろうけど)。

からの入れ子3つ

というわけで、入れ子3つにしてみた:

def simply_for_2(n):
    a = list()
    for i in range(n):
        for j in range(n):
            for k in range(n):
                if i+j+k % 2 == 0:
                    a.append(i+j+k)
    return a


def comprehension_2(n):
    a = [i+j+k for i in range(n) for j in range(n) for k in range(n)
         if i+j+k % 2 == 0]
    return a


def generator_2(n):
    for i in range(n):
        for j in range(n):
            for k in range(n):
                if i+j+k % 2 == 0:
                    yield i+j+k


def generator_case_2(n):
    a = list(generator_2(n))
    return a


if __name__ == '__main__':
    n = 300
    for func in (simply_for_2, comprehension_2,
                 generator_case_2):
        print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
simply_for_2: 8.179sec.
comprehension_2: 7.936sec.
generator_case_2: 7.853sec.

び、びみょ~~~~~~~~~~~~~~~( ᕦ 。 ᕤ)

入れ子が増えると内包表記もfor並にとろくなるのかしら。そう考えると、コードの見た目の観点から普通にfor書くべきだね。

ジェネレータは安定して、「微妙に」速い。

併用や!

見た目的にも、かつ、汎用性も兼ねることを考えると、ジェネレータと内包表記を併用するのが良さそう?

def generator_3(n):
    for i in range(n):
        for j in range(n):
            for k in range(n):
                yield i, j, k


def generator_and_comprehension(n):
    a = [i+j+k for i, j, k in generator_3(n) if i+j+k % 2 == 0]
    return a

if __name__ == '__main__':
    n = 250
    for func in (simply_for_2, comprehension_2,
                 generator_case_2, generator_and_comprehension):
        print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
simply_for_2: 4.445sec.
comprehension_2: 4.476sec.
generator_case_2: 4.380sec.
generator_and_comprehension: 7.460sec.

ひえっ( ᕦ 。 ᕤ)お、おしょい・・・!

まあ恐らく原因は、forが実質1つ増えてるというところでしょうかな……。

ジェネレータで入れ子forを分離してやるというのは、個人的には見た目すごいスッキリして好きなんだけどな…。

ちなみに、

def generator_and_for(n):
    a = []
    for i, j, k in generator_3(n):
        if i+j+k % 2 == 0:
            a.append(i+j+k)
    return a

if __name__ == '__main__':
    n = 250
    for func in (generator_and_comprehension, generator_and_for):
        print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
generator_and_comprehension: 7.505sec.
generator_and_for: 7.465sec.

なので、もうジェネレータ律速って感じがしますね。内包表記の速みはどこにいったのか……?

itertoolsや!

from itertools import product


def product_and_for(n):
    a = []
    for i, j, k in product(range(n), range(n), range(n)):
        if i+j+k % 2 == 0:
            a.append(i+j+k)
    return a


def product_and_comprehension(n):
    a = [i+j+k for i, j, k in product(range(n), range(n), range(n))
         if i+j+k % 2 == 0]

if __name__ == '__main__':
    n = 250
    for func in (generator_and_comprehension, generator_and_for,
                 product_and_for, product_and_comprehension):
        print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))

いけー!ぷいきゅあがんばぇー!

generator_and_comprehension: 7.478sec.
generator_and_for: 7.412sec.
product_and_for: 4.933sec.
product_and_comprehension: 4.959sec.

!!

普通に回すよりは若干遅くなるものの、product の力が伺えますね。これが標準ライブラリ(というかC)のちからか…!

というわけで

単一 for なら内包表記で、入れ子なら itertools.product 使って単一 for で回すのがキレイにそこそこ速そう。

10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.5.2 ドキュメント

ちなみに、product(range(n), range(n), range(n))product(range(n), repeat=3) とも書けます。

ちなみに

単一 for でも、回したいシーケンスが range では上手く表現できないときは、内包表記渡すかジェネレータ式渡すか、ですが、メモリ的なこと考えるとジェネレータ式渡したほうがよさそうではある?

面倒なのでやりませんけどね!😇

てか

なんでこんなに内包表記が遅くなったんでしょ?

def if_for(n):
    a = list()
    for i in range(n):
        if i % 2 == 0:
            a.append(i)
    return a


def if_comprehension(n):
    a = [i for i in range(n) if i % 2 == 0]
    return a

if __name__ == '__main__':
    n = 10**7
    # for func in (generator_and_comprehension, generator_and_for,
    #              product_and_for, product_and_comprehension):
    for func in (if_for, if_comprehension):
        print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
if_for: 2.967sec.
if_comprehension: 2.430sec.

ふむふむ。内包表記 if いれるだけでこんなに時間が接近するとは。

もしかして、入れ子がダメなのか?

def for_for(n):
    a = list()
    for i in range(n):
        for j in range(n):
            a.append(i+j)
    return a


def comp_comprehension(n):
    a = [i+j for i in range(n) for j in range(n)]
    return a

if __name__ == '__main__':
    n = 3500
    # for func in (generator_and_comprehension, generator_and_for,
    #              product_and_for, product_and_comprehension):
    for func in (for_for, comp_comprehension):
        print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
for_for: 3.454sec.
comp_comprehension: 2.177sec.

ほほう🤔約1.5倍なので、これは最初にやった単一forと変わらないですね。

内包表記、if 入れると遅くなるのか……?

結論

if はヤバイ(?)

*1:桁を切れ