forループとリスト内包表記の実行時間の差を計測


Python forループとリスト内包表記の実行時間の差を計測 - 複雑系スパゲティソース(はてな版)


で、内包表記の方が、forループより10倍速いという結果が出ていたのですが、
疑問のコメントが出ていたので、自分でも測ってみました。



まず、forループと内包表記で値が異なっていたので、統一。

あとprintをすると、forループと内包表記の純粋な比較にはならない上、
画面が流れてしまって測定結果が見づらいので、printは無くします。


でも、やっぱり、forループの方が遅かったのですが、
上手い書き方をすればもっとマシになるか、と、色々いじってみました。

#encoding:shift-jis
from __future__ import with_statement, division, print_function
import datetime
from timeit import Timer
N = 20

def test_for_loop():
    #forループ
    listall = []
    for first_rc in range(N):
        for second_rc in range(N):
            for third_rc in range(N):
                listall = listall + [third_rc]
    return listall

def test_comprehension():
    #リスト内包表記
    return [third_rc for first_rc in range(N)
                for second_rc in range(N)
                    for third_rc in range(N)]

def test_for_loop_append():
    #forループ 加算ではなくappendを使用
    listall = []
    for first_rc in range(N):
        for second_rc in range(N):
            for third_rc in range(N):
                listall.append(third_rc)
    return listall

def test_for_loop_append2():
    #forループ appendをローカル変数に代入
    listall = []
    listall_append = listall.append
    for first_rc in range(N):
        for second_rc in range(N):
            for third_rc in range(N):
                listall_append(third_rc)
    return listall

def test_for_loop_extend():
    #forループ appendをextendに変更
    listall = []
    listall_extend = listall.extend
    for first_rc in range(N):
        for second_rc in range(N):
            listall_extend(range(N))
    return listall

def test_for_loop_xrange():
    #forループ rangeをxrangeに変更
    listall = []
    listall_extend = listall.extend
    for first_rc in xrange(N):
        for second_rc in xrange(N):
            listall_extend(xrange(N))
    return listall

def test_comprehension_xrange():
    #リスト内包表記 rangeをxrangeに変更
    return [third_rc for first_rc in xrange(N)
                for second_rc in xrange(N)
                    for third_rc in xrange(N)]

def test_times():
    #素直にかけ算!
    return range(N)*(N*N)


test_functions = [
    test_for_loop,
    test_comprehension,
    test_for_loop_append,
    test_for_loop_append2,
    test_for_loop_extend,
    test_for_loop_xrange,
    test_comprehension_xrange, 
    test_times,
]

for f in test_functions:
    assert f() == test_for_loop(), str(f)

count = 100
for f in test_functions:
    print(f.__name__, Timer(f).timeit(count))

結果










































forループ test_for_loop 390.356245556
リスト内包表記 test_comprehension 1.23270108352
forループ 加算ではなくappendを使用 test_for_loop_append 2.46575807819
forループ appendをローカル変数に代入 test_for_loop_append2 1.70131902239
forループ appendをextendに変更 test_for_loop_extend 0.550672222308
forループ rangeをxrangeに変更 test_for_loop_xrange 0.487926258784
リスト内包表記 rangeをxrangeに変更 test_comprehension_xrange 1.03056813087
素直にかけ算! test_times 0.0899770780923


まず、オリジナルのforループがおっそろしく遅いです。
一々、listall = listall + [third_rc]で、
新しくリストを作っているので。


普通、追加にはappendを使うので、そのようにしたら、
比較できる程度には早くなりました。


次に、毎回、appendメソッドを属性一覧の中から探してくるのは時間がかかるので、
ローカル変数に代入して、時間を稼ぎます。


また、今回は3番目のループは、中身をそのまま追加しているだけなので、
extendに変更します。


そして、巨大なリストを生成してメモリを消費するrangeを、
リストを生成しないxrangeに変更・・・って、これはextendとか、
メソッドを変数に代入する前にやるべきですよね(笑)。


これは内包表記でも使える方法です。





改良の結果、なんと内包表記よりforループの方が速くなりました!


変数を評価する回数が減ったからでしょうかね?




しかし、もっとも速かったのは素直にかけ算!


いや、もちろん、実際的なコードでは使えないでしょうけど、
ヘタに凝ったコードを書くよりも、シンプルな方が良い事もあるんですね。