Liste kavrayışları ve işlevsel işlevler “döngüler için” daha hızlı mıdır?


155

Python performans açısından, bir liste-anlama, ya da fonksiyonlar gibi map(), filter()ve reduce()daha hızlı döngü için daha? Neden teknik olarak, C döngüsü python sanal makine hızında çalışırken neden C hızında çalışırlar ?

Diyelim ki geliştirdiğim bir oyunda döngüler için karmaşık ve devasa haritalar çizmem gerekiyor. Bu soru kesinlikle alakalı olacaktır, çünkü örneğin bir liste kavraması gerçekten daha hızlıysa, gecikmeleri önlemek için çok daha iyi bir seçenek olurdu (Kodun görsel karmaşıklığına rağmen).

Yanıtlar:


147

Aşağıdakiler deneyime dayanan kaba kurallar ve eğitimli tahminlerdir. timeitSert sayılar elde etmek için somut kullanım durumunuzu belirtmelisiniz veya profilinize eklemelisiniz ve bu sayılar zaman zaman aşağıdakilerle aynı fikirde olmayabilir.

Bir liste kavraması genellikle tam olarak eşdeğer fordöngüden (aslında bir liste oluşturur) biraz daha hızlıdır , büyük olasılıkla appendher yinelemede listeye ve yöntemine bakmak zorunda değildir . Ancak, bir liste kavrama hala bayt kodu düzeyinde bir döngü yapar:

>>> 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

Bir döngü yerine bir liste anlama kullanarak gelmez nonsensically anlamsız değerler listesi biriken ve uzağa listeyi atma, bir liste oluşturmak, genellikle yavaştır , çünkü oluşturma ve listeyi uzatmanın yükü. Liste kavrayışları, iyi bir eski döngüden daha hızlı olan sihir değildir.

Fonksiyonel liste işleme fonksiyonları gelince: Bu C ile yazılmış ve muhtemelen Python ile yazılmış eşdeğer işlevleri daha iyi performans olsa da, bunlar değil mutlaka en hızlı seçenek. Bazı hız yukarıya bekleniyor eğer fonksiyon çok C ile yazılmış. Ancak lambda(veya başka bir Python işlevi) kullanan çoğu durumda , Python yığın çerçevelerini tekrar tekrar kurma yükü herhangi bir tasarruf sağlar. Aynı işi hat üzerinde, fonksiyon çağrıları olmadan yapmak (örneğin, mapveya yerine bir liste anlama filter) genellikle biraz daha hızlıdır.

Diyelim ki geliştirdiğim bir oyunda döngüler için karmaşık ve devasa haritalar çizmem gerekiyor. Bu soru kesinlikle alakalı olacaktır, çünkü örneğin bir liste kavraması gerçekten daha hızlıysa, gecikmeleri önlemek için çok daha iyi bir seçenek olurdu (Kodun görsel karmaşıklığına rağmen).

Muhtemelen, bu tür kod iyi "optimize edilmemiş" Python ile yazıldığında zaten yeterince hızlı değilse, hiçbir miktarda Python seviyesi mikro optimizasyonu yeterince hızlı yapmaz ve C'ye düşmeyi düşünmeye başlamalısınız. mikro optimizasyonlar genellikle Python kodunu önemli ölçüde hızlandırabilir, bunun için düşük (mutlak terimlerle) bir sınır vardır. Dahası, o tavana çarpmadan önce bile, mermi ısırmak ve biraz C yazmak için sadece daha uygun maliyetli hale gelir (% 15 hızlanma ve aynı çabayla% 300 hızlanma).


25

Python.org'daki bilgileri kontrol ederseniz , bu özeti görebilirsiniz:

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

Ama gerçekten gereken performans farkının nedenini anlamak için ayrıntılar yukarıdaki makale okudum.

Ben de şiddetle timeit kullanarak kod zaman gerekir öneririz . Günün sonunda, örneğin forbir koşul karşılandığında döngüden çıkmanız gerekebilecek bir durum olabilir . Potansiyel olarak arayarak sonucu bulmaktan daha hızlı olabilir map.


17
Bu sayfa iyi bir okuma ve kısmen ilgili olsa da, bu sayıları alıntılamak yararlı olmayabilir, hatta yanıltıcı bile olabilir.

1
Bu, ne zamanladığınıza dair bir gösterge vermez. Göreceli performans, döngü / listcomp / haritadakine bağlı olarak büyük ölçüde değişecektir.
user2357112 Monica

@delnan katılıyorum. Performanstaki farkı anlamak için OP'yi belgeleri okumaya çağırmak için cevabımı değiştirdim.
Anthony Kong

@ user2357112 Bağlam için bağladığım wiki sayfasını okumalısınız. OP referansı için yayınladım.
Anthony Kong

13

Özellikle hakkında soruyorsunuz map(), filter()ve reduce()genel olarak fonksiyonel programlama hakkında bilmek istediğinizi varsayalım. Bunu kendimi bir nokta kümesi içindeki tüm noktalar arasındaki mesafeleri hesaplama problemi üzerinde test ettikten sonra, fonksiyonel programlama ( starmapdahili itertoolsmodülün fonksiyonunu kullanarak ) döngülerden biraz daha yavaş (1.25 kat uzunluğunda, aslında). İşte kullandığım örnek kod:

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

İşlevsel sürüm yordamsal sürümden daha hızlı mı?

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
Bu soruyu cevaplamak için oldukça kıvrımlı bir yol gibi görünüyor. Daha iyi mantıklı hale getirmek için pare yapabilir misiniz?
Aaron Hall

2
@AaronHall Aslında andreipmbcn'ın cevabını oldukça ilginç buluyorum çünkü önemsiz bir örnek. Kod ile oynayabiliriz.
Anthony Kong

@AaronHall, daha açık ve anlaşılır bir şekilde görünmesi için metin paragrafını düzenlememi ister misiniz, yoksa kodu düzenlememi ister misiniz?
andreipmbcn

9

Hızı test eden basit bir senaryo yazdım ve bunu öğrendim. Aslında döngü için benim durumumda en hızlı oldu. Bu beni gerçekten şaşırttı, aşağıdaki karelere göz at (karelerin toplamını hesaplıyordu).

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 ile farklılıklar çok büyük değil; Küçült ve Harita 0,24'e, kavrayışı da 0,29'a kadar listeliyor. Çünkü daha yüksek, 0.18'de.
jjmerelo

Ortadan kaldırmak intiçinde square_sum4de biraz daha hızlı ve sadece biraz daha yavaş döngü için yapar.
jjmerelo

6

@ Alisa kodunu değiştirdim ve cProfileliste kavramanın neden daha hızlı olduğunu göstermek için kullandım :

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()

İşte sonuçlar:

=========================
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}

BENİM NACİZANE FİKRİME GÖRE:

  • reduceve mapgenel olarak oldukça yavaş. Sadece bu da değil sum, mapgeri dönen yineleyicilerin kullanımı sumbir listeye kıyasla yavaş
  • for_loop bir dereceye kadar yavaş olan append kullanır
  • liste-kavrama sadece listeyi oluşturmak için en az zaman harcamakla kalmaz sum, aksinemap

5

Alphii cevabına bir bükülme eklerken , for döngüsü ikinci en iyi ikinci ve yaklaşık 6 kat daha yavaş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])

Ana değişiklikler yavaş sumçağrıları int()ve son durumda muhtemelen gereksiz olanları ortadan kaldırmak olmuştur . For döngüsü ve haritayı aynı terimlerle koymak aslında bunu oldukça gerçeğe dönüştürüyor. Lambdaların işlevsel kavramlar olduğunu ve teorik olarak yan etkilere sahip olmaması gerektiğini unutmayın, ancak, ekleme gibi yan etkilere sahip olabilirlera . Bu durumda Python 3.6.1, Ubuntu 14.04, Intel (R) Core (TM) i7-4770 CPU @ 3.40GHz ile sonuçlanır

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 ve square_sum4 yanlış. Toplam vermeyecekler. @Alisca chen aşağıdaki cevap aslında doğrudur.
ShikharDua

3

Bazı @ alpiii kodunu değiştirmeyi başardı ve Liste kavrama döngü için biraz daha hızlı olduğunu keşfetti. Bunun nedeni int()liste kavrayışı ile döngü arasında adil değildir.

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.