Apakah daftar-pemahaman dan fungsi fungsional lebih cepat daripada "untuk loop"?

155

Dalam hal kinerja dalam Python, apakah daftar-pemahaman, atau fungsi suka map(), filter()dan reduce()lebih cepat daripada untuk loop? Mengapa, secara teknis, mereka berjalan dalam kecepatan C , sedangkan untuk loop berjalan dalam kecepatan mesin virtual python ?

Misalkan dalam game yang saya kembangkan saya perlu menggambar peta yang rumit dan besar menggunakan untuk loop. Pertanyaan ini pasti relevan, karena jika pemahaman daftar, misalnya, memang lebih cepat, itu akan menjadi pilihan yang jauh lebih baik untuk menghindari keterlambatan (Meskipun kompleksitas visual dari kode).

Ericson Willians
sumber

Jawaban:

146

Berikut ini adalah pedoman kasar dan tebakan berdasarkan pengalaman. Kamu harustimeit atau profil kasus penggunaan konkret Anda untuk mendapatkan angka keras, dan angka-angka itu kadang-kadang mungkin tidak setuju dengan di bawah ini.

Pemahaman daftar biasanya sedikit lebih cepat daripada forloop yang sama persis (yang sebenarnya membangun daftar), kemungkinan besar karena tidak harus mencari daftar dan appendmetode pada setiap iterasi. Namun, pemahaman daftar masih melakukan loop tingkat-bytecode:

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

Menggunakan pemahaman daftar di tempat loop itu tidak membangun daftar, secara tidak masuk akal mengumpulkan daftar nilai yang tidak berarti dan kemudian membuang daftar itu, seringkali lebih lambat karena overhead menciptakan dan memperluas daftar. Pemahaman daftar bukan sihir yang secara inheren lebih cepat daripada loop lama yang baik.

Adapun fungsi pemrosesan daftar fungsional: Meskipun ini ditulis dalam C dan mungkin mengungguli fungsi setara yang ditulis dengan Python, mereka tidak selalu merupakan opsi tercepat. Beberapa percepatan diharapkan jika fungsi tersebut ditulis dalam C juga. Tetapi kebanyakan kasus menggunakan alambda (atau fungsi Python lainnya), overhead berulang kali mengatur frame tumpukan Python dll memakan penghematan. Cukup melakukan pekerjaan in-line yang sama, tanpa pemanggilan fungsi (mis. Pemahaman daftar alih-alih mapatau filter) seringkali sedikit lebih cepat.

Misalkan dalam game yang saya kembangkan saya perlu menggambar peta yang rumit dan besar menggunakan untuk loop. Pertanyaan ini pasti relevan, karena jika pemahaman daftar, misalnya, memang lebih cepat, itu akan menjadi pilihan yang jauh lebih baik untuk menghindari keterlambatan (Meskipun kompleksitas visual dari kode).

Kemungkinannya adalah, jika kode seperti ini belum cukup cepat ketika ditulis dengan Python non-"baik" yang baik, tidak ada jumlah optimasi mikro tingkat Python akan membuatnya cukup cepat dan Anda harus mulai berpikir untuk menjatuhkan ke C. Sementara luas optimasi mikro sering dapat mempercepat kode Python, ada batas rendah (dalam hal absolut) untuk ini. Selain itu, bahkan sebelum Anda mencapai langit-langit itu, menjadi lebih hemat biaya (percepatan 15% vs 300% dengan upaya yang sama) untuk menggigit peluru dan menulis beberapa huruf C.


sumber
25

Jika Anda memeriksa info di python.org , Anda dapat melihat ringkasan ini:

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

Tetapi Anda harus melakukannya membaca artikel di atas secara terperinci untuk memahami penyebab perbedaan kinerja.

Saya juga sangat menyarankan Anda harus menghitung waktu kode Anda dengan menggunakan timeit . Pada akhirnya, mungkin ada situasi di mana, misalnya, Anda mungkin harus keluar dari forlingkaran ketika suatu kondisi terpenuhi. Ini bisa berpotensi lebih cepat daripada mencari tahu hasilnya dengan menelepon map.

Anthony Kong
sumber
17
Sementara halaman itu adalah bacaan yang baik dan sebagian terkait, hanya mengutip angka-angka itu tidak membantu, bahkan mungkin menyesatkan.
1
Ini tidak memberikan indikasi apa yang Anda pilih. Performa relatif akan sangat bervariasi tergantung pada apa yang ada di loop / listcomp / map.
user2357112 mendukung Monica
@nannan saya setuju. Saya telah memodifikasi jawaban saya untuk mendesak OP agar membaca dokumentasi untuk memahami perbedaan dalam kinerja.
Anthony Kong
@ user2357112 Anda harus membaca halaman wiki yang saya tautkan untuk konteksnya. Saya mempostingnya untuk referensi OP.
Anthony Kong
13

Anda bertanya secara spesifik tentang map(), filter()dan reduce(), tetapi saya berasumsi Anda ingin tahu tentang pemrograman fungsional secara umum. Setelah menguji ini sendiri pada masalah jarak komputasi antara semua titik dalam satu set poin, pemrograman fungsional (menggunakan starmapfungsi dari itertoolsmodul built-in ) ternyata sedikit lebih lambat daripada for-loop (mengambil 1,25 kali lebih lama, dalam fakta). Berikut ini contoh kode yang saya gunakan:

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

Apakah versi fungsional lebih cepat daripada versi prosedural?

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')
andreipmbcn
sumber
2
Sepertinya cara yang agak berbelit-belit untuk menjawab pertanyaan ini. Bisakah Anda menguranginya sehingga lebih masuk akal?
Aaron Hall
2
@AaronHall, saya benar-benar menemukan jawaban andreipmbcn agak menarik karena ini adalah contoh non-sepele. Kode dapat kita mainkan.
Anthony Kong
@ AaronHall, apakah Anda ingin saya mengedit paragraf teks sehingga terdengar lebih jelas dan mudah, atau Anda ingin saya mengedit kode?
andreipmbcn
9

Saya menulis naskah sederhana yang menguji kecepatan dan inilah yang saya temukan. Sebenarnya untuk loop adalah yang tercepat dalam kasus saya. Itu benar-benar mengejutkan saya, lihat di bawah (sedang menghitung jumlah kotak).

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
alphiii
sumber
Dengan python 3.6.1 perbedaannya tidak terlalu besar; Kurangi dan Peta turun ke 0,24 dan daftar pemahaman ke 0,29. Untuk lebih tinggi, di 0,18.
jjmerelo
Menghilangkan intin square_sum4juga membuatnya sedikit lebih cepat dan hanya sedikit lebih lambat daripada for loop.
jjmerelo
6

Saya memodifikasi kode @ Alisa dan digunakan cProfileuntuk menunjukkan mengapa pemahaman daftar lebih cepat:

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

Inilah hasilnya:

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

MENURUT OPINI SAYA:

  • reducedan mapsecara umum cukup lambat. Tidak hanya itu, penggunaan sumpada iterator yang mapdikembalikan lambat, dibandingkan sumdengan daftar
  • for_loop menggunakan append, yang tentu saja lambat sampai batas tertentu
  • daftar-pemahaman tidak hanya menghabiskan sedikit waktu membangun daftar, itu juga membuat sumlebih cepat, berbeda denganmap
tjysdsg
sumber
5

Menambahkan twist pada jawaban Alphii , sebenarnya for loop akan menjadi yang terbaik kedua dan sekitar 6 kali lebih lambat daripadamap

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

Perubahan utama adalah untuk menghilangkan sumpanggilan lambat , serta mungkin tidak perlu int()dalam kasus terakhir. Menempatkan for for dan map dalam istilah yang sama membuatnya menjadi fakta, sebenarnya. Ingat bahwa lambda adalah konsep fungsional dan secara teori seharusnya tidak memiliki efek samping, tetapi, well, mereka dapat memiliki efek samping seperti menambahkan a. Hasil dalam hal ini dengan 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
jjmerelo
sumber
2
square_sum3 dan square_sum4 salah. Mereka tidak akan memberikan jumlah. Jawaban di bawah ini dari @alisca chen sebenarnya benar.
ShikharDua
3

Saya telah berhasil memodifikasi beberapa kode @ alpiii dan menemukan bahwa pemahaman Daftar sedikit lebih cepat daripada untuk loop. Mungkin disebabkan oleh int(), tidak adil antara pemahaman daftar dan untuk loop.

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
Alisca Chen
sumber