リスト内包表記と関数関数は、「forループ」よりも高速ですか?


155

Pythonでパフォーマンスの面では、リスト内包表記である、または機能が好きでmap()filter()そしてreduce()速くforループよりも?forループがPython仮想マシンの速度で実行されるのに対し、技術的にはCの速度実行されるのはなぜですか?

開発中のゲームで、forループを使用して複雑で巨大なマップを描画する必要があるとします。この質問は間違いなく関連性があります。たとえば、リスト内包が実際に高速である場合、(コードの視覚的な複雑性にもかかわらず)ラグを回避するためのはるかに優れたオプションです。

回答:


146

以下は、大まかなガイドラインと経験に基づく知識に基づく推測です。明確timeitな数値を取得するには、具体的なユースケースをプロファイリングする必要があります。これらの数値は、以下に同意しない場合があります。

通常、リスト内包表記は、正確に同等のforループ(実際にリストを作成する)よりも少し高速ですappend。これは、反復のたびにリストとそのメソッドを検索する必要がないためです。ただし、リスト内包表記でもバイトコードレベルのループが実行されます。

>>> dis.dis(<the code object for `[x for x in range(10)]`>)
 1           0 BUILD_LIST               0
             3 LOAD_FAST                0 (.0)
       >>    6 FOR_ITER                12 (to 21)
             9 STORE_FAST               1 (x)
            12 LOAD_FAST                1 (x)
            15 LIST_APPEND              2
            18 JUMP_ABSOLUTE            6
       >>   21 RETURN_VALUE

リストを作成しないループの代わりにリスト内包表記を使用し、無意味な値のリストを無意味に蓄積してからリストを破棄すると、リストの作成と拡張のオーバーヘッドのため、多くの場合遅くなります。リスト内包表記は、古き良きループよりも本質的に高速な魔法ではありません。

関数リスト処理関数について:これらはCで記述され、おそらくPythonで記述された同等の関数よりも優れていますが必ずしも最速のオプションとは限りません。関数もCで記述されている場合は、ある程度の高速化が期待されます。しかし、ほとんどの場合、lambda(または他のPython関数)、Pythonスタックフレームなどを繰り返し設定するオーバーヘッドにより、すべての節約が無駄になります。関数を呼び出さずに同じ作業をインラインで行うだけ(たとえば、mapまたはの代わりにリスト内包表記filter)の方が、多くの場合わずかに高速です。

開発中のゲームで、forループを使用して複雑で巨大なマップを描画する必要があるとします。この質問は間違いなく関連性があります。たとえば、リスト内包が実際に高速である場合、(コードの視覚的な複雑性にもかかわらず)ラグを回避するためのはるかに優れたオプションです。

このようなコードが「最適化されていない」優れたPythonで記述されたときに十分に高速でない場合、Pythonレベルのマイクロ最適化で十分な速度が得られないため、Cへの移行を検討する必要があります。多くの場合、マイクロ最適化はPythonコードを大幅に高速化できます。これには(絶対的に)低い制限があります。さらに、その上限に達する前であっても、弾丸を噛んでCを書く方が、費用対効果が高くなります(15%高速化と同じ労力で300%高速化)。


25

python.org情報を確認すると、次の概要を確認できます。

Version Time (seconds)
Basic loop 3.47
Eliminate dots 2.45
Local variable & no dots 1.79
Using map function 0.54

ただし、パフォーマンスの違いの原因を理解するには、上記の記事を詳細に読む必要があります。

また、timeitを使用してコードの時間を測定することを強くお勧めします。1日の終わりに、たとえば、for条件が満たされたときにループを抜ける必要がある場合があります。これは、を呼び出して結果を見つけるよりも速くなる可能性がありますmap


17
そのページは読みやすく、部分的に関連していますが、これらの数字を引用するだけでは役に立ちません。誤解を招く可能性さえあります。

1
これはあなたが何を計っているのかを示すものではありません。相対的なパフォーマンスは、loop / listcomp / mapの内容によって大きく異なります。
user2357112は

@delnan同意する。OPがドキュメントを読んでパフォーマンスの違いを理解するように促すため、私の回答を変更しました。
Anthony Kong

@ user2357112コンテキストにリンクしたwikiページを読む必要があります。OPの参考用に投稿しました。
アンソニーコング

13

あなたは具体的には約尋ねるmap()filter()そしてreduce()、私はあなたが一般的な関数型プログラミングについて知りたいと仮定します。一連のポイント内のすべてのポイント間の距離を計算する問題についてこれを自分でテストしたところ、関数型プログラミング(starmap組み込みitertoolsモジュールの関数を使用)はforループよりも少し遅いことがわかっています(1.25倍の時間がかかります)。事実)。これが私が使用したサンプルコードです:

import itertools, time, math, random

class Point:
    def __init__(self,x,y):
        self.x, self.y = x, y

point_set = (Point(0, 0), Point(0, 1), Point(0, 2), Point(0, 3))
n_points = 100
pick_val = lambda : 10 * random.random() - 5
large_set = [Point(pick_val(), pick_val()) for _ in range(n_points)]
    # the distance function
f_dist = lambda x0, x1, y0, y1: math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2)
    # go through each point, get its distance from all remaining points 
f_pos = lambda p1, p2: (p1.x, p2.x, p1.y, p2.y)

extract_dists = lambda x: itertools.starmap(f_dist, 
                          itertools.starmap(f_pos, 
                          itertools.combinations(x, 2)))

print('Distances:', list(extract_dists(point_set)))

t0_f = time.time()
list(extract_dists(large_set))
dt_f = time.time() - t0_f

機能バージョンは手続きバージョンよりも高速ですか?

def extract_dists_procedural(pts):
    n_pts = len(pts)
    l = []    
    for k_p1 in range(n_pts - 1):
        for k_p2 in range(k_p1, n_pts):
            l.append((pts[k_p1].x - pts[k_p2].x) ** 2 +
                     (pts[k_p1].y - pts[k_p2].y) ** 2)
    return l

t0_p = time.time()
list(extract_dists_procedural(large_set)) 
    # using list() on the assumption that
    # it eats up as much time as in the functional version

dt_p = time.time() - t0_p

f_vs_p = dt_p / dt_f
if f_vs_p >= 1.0:
    print('Time benefit of functional progamming:', f_vs_p, 
          'times as fast for', n_points, 'points')
else:
    print('Time penalty of functional programming:', 1 / f_vs_p, 
          'times as slow for', n_points, 'points')

2
この質問に答えるのはかなり複雑な方法のように見えます。それを理解しやすくすることができますか?
アーロンホール

2
@AaronHall実際にはandreipmbcnの答えは興味深いものです。私たちが遊ぶことができるコード。
Anthony Kong

@AaronHall、テキストパラグラフを編集してより明瞭で簡単に聞こえるようにしますか、それともコードを編集しますか?
andreipmbcn 2014年

9

私は速度をテストする簡単なスクリプトを書いて、これが私が見つけたものです。実際、私の場合はforループが最速でした。それは本当に私を驚かせました、以下をチェックしてください(二乗和を計算していました)。

from functools import reduce
import datetime


def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next**2, numbers, 0)


def square_sum2(numbers):
    a = 0
    for i in numbers:
        i = i**2
        a += i
    return a

def square_sum3(numbers):
    sqrt = lambda x: x**2
    return sum(map(sqrt, numbers))

def square_sum4(numbers):
    return(sum([int(i)**2 for i in numbers]))


time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
0:00:00.302000 #Reduce
0:00:00.144000 #For loop
0:00:00.318000 #Map
0:00:00.390000 #List comprehension

Python 3.6.1では、違いはそれほど大きくありません。リデュースとマップは0.24に下がり、理解度は0.29にリストされます。0.18でより高いです。
jjmerelo 2018年

intinを削除square_sum4すると、forループよりもかなり速く、少し遅くなります。
jjmerelo 2018年

6

@Alisaのコードを変更しcProfile、リストの理解がより速い理由を示すために使用しました。

from functools import reduce
import datetime

def reduce_(numbers):
    return reduce(lambda sum, next: sum + next * next, numbers, 0)

def for_loop(numbers):
    a = []
    for i in numbers:
        a.append(i*2)
    a = sum(a)
    return a

def map_(numbers):
    sqrt = lambda x: x*x
    return sum(map(sqrt, numbers))

def list_comp(numbers):
    return(sum([i*i for i in numbers]))

funcs = [
        reduce_,
        for_loop,
        map_,
        list_comp
        ]

if __name__ == "__main__":
    # [1, 2, 5, 3, 1, 2, 5, 3]
    import cProfile
    for f in funcs:
        print('=' * 25)
        print("Profiling:", f.__name__)
        print('=' * 25)
        pr = cProfile.Profile()
        for i in range(10**6):
            pr.runcall(f, [1, 2, 5, 3, 1, 2, 5, 3])
        pr.create_stats()
        pr.print_stats()

結果は次のとおりです。

=========================
Profiling: reduce_
=========================
         11000000 function calls in 1.501 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.162    0.000    1.473    0.000 profiling.py:4(reduce_)
  8000000    0.461    0.000    0.461    0.000 profiling.py:5(<lambda>)
  1000000    0.850    0.000    1.311    0.000 {built-in method _functools.reduce}
  1000000    0.028    0.000    0.028    0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: for_loop
=========================
         11000000 function calls in 1.372 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.879    0.000    1.344    0.000 profiling.py:7(for_loop)
  1000000    0.145    0.000    0.145    0.000 {built-in method builtins.sum}
  8000000    0.320    0.000    0.320    0.000 {method 'append' of 'list' objects}
  1000000    0.027    0.000    0.027    0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: map_
=========================
         11000000 function calls in 1.470 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.264    0.000    1.442    0.000 profiling.py:14(map_)
  8000000    0.387    0.000    0.387    0.000 profiling.py:15(<lambda>)
  1000000    0.791    0.000    1.178    0.000 {built-in method builtins.sum}
  1000000    0.028    0.000    0.028    0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: list_comp
=========================
         4000000 function calls in 0.737 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.318    0.000    0.709    0.000 profiling.py:18(list_comp)
  1000000    0.261    0.000    0.261    0.000 profiling.py:19(<listcomp>)
  1000000    0.131    0.000    0.131    0.000 {built-in method builtins.sum}
  1000000    0.027    0.000    0.027    0.000 {method 'disable' of '_lsprof.Profiler' objects}

私見では:

  • reduceそしてmap、一般的にかなり遅いです。それだけでなくsummap返されたイテレータでの使用はsum、リストのing に比べて遅い
  • for_loop 追加を使用しますが、これはもちろんある程度低速です
  • list-comprehensionは、リストの作成に最短の時間を費やしただけでなく、それsumとは対照的にはるかに速くなりますmap

5

アルフィイの答えにひねりを加えると、実際にはforループが2番目に良くなり、約6倍遅くなりますmap

from functools import reduce
import datetime


def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next**2, numbers, 0)


def square_sum2(numbers):
    a = 0
    for i in numbers:
        a += i**2
    return a

def square_sum3(numbers):
    a = 0
    map(lambda x: a+x**2, numbers)
    return a

def square_sum4(numbers):
    a = 0
    return [a+i**2 for i in numbers]

time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])

主な変更は、遅いsum呼び出しと、おそらくint()最後のケースではおそらく不要な呼び出しを排除することです。forループとマップを同じ用語で置くと、実際にはかなり事実になります。そのラムダがよく、彼らは、機能的な概念であり、理論的には副作用を持つべきではないが、覚えておいてくださいすることができますに追加するなどの副作用がありますa。この場合の結果は、Python 3.6.1、Ubuntu 14.04、Intel(R)Core(TM)i7-4770 CPU @ 3.40GHzです。

0:00:00.257703 #Reduce
0:00:00.184898 #For loop
0:00:00.031718 #Map
0:00:00.212699 #List comprehension

2
square_sum3とsquare_sum4は正しくありません。彼らは合計を与えません。@aliscaからの以下の回答chenは実際に正しいです。
ShikharDua

3

私はなんとか@alpiiiのコードを変更し、リスト内包表記がforループよりも少し速いことを発見しました。それはによって引き起こされる可能性がありint()、リストの理解とforループの間で公平ではありません。

from functools import reduce
import datetime

def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next*next, numbers, 0)

def square_sum2(numbers):
    a = []
    for i in numbers:
        a.append(i*2)
    a = sum(a)
    return a

def square_sum3(numbers):
    sqrt = lambda x: x*x
    return sum(map(sqrt, numbers))

def square_sum4(numbers):
    return(sum([i*i for i in numbers]))

time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
0:00:00.101122 #Reduce

0:00:00.089216 #For loop

0:00:00.101532 #Map

0:00:00.068916 #List comprehension
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.