Việc hiểu danh sách và các chức năng chức năng có nhanh hơn so với các vòng lặp không?


155

Về mặt hiệu năng trong Python, là một sự hiểu biết danh sách, hoặc các chức năng như map(), filter()reduce()nhanh hơn một vòng lặp for? Tại sao, về mặt kỹ thuật, chúng chạy ở tốc độ C , trong khi vòng lặp for chạy ở tốc độ máy ảo python ?

Giả sử trong một trò chơi mà tôi đang phát triển, tôi cần vẽ các bản đồ phức tạp và khổng lồ bằng cách sử dụng các vòng lặp. Câu hỏi này chắc chắn có liên quan, ví dụ, nếu hiểu toàn bộ danh sách, thực sự nhanh hơn, thì đó sẽ là một lựa chọn tốt hơn nhiều để tránh độ trễ (Mặc dù độ phức tạp về hình ảnh của mã).

Câu trả lời:


146

Sau đây là những hướng dẫn sơ bộ và phỏng đoán có giáo dục dựa trên kinh nghiệm. Bạn nên timeithoặc hồ sơ trường hợp sử dụng cụ thể của bạn để có được số cứng, và những số đó đôi khi có thể không đồng ý với dưới đây.

Khả năng hiểu danh sách thường nhanh hơn một chút so với forvòng lặp tương đương chính xác (thực sự xây dựng danh sách), rất có thể vì nó không phải tìm danh sách và appendphương thức của nó trên mỗi lần lặp. Tuy nhiên, việc hiểu danh sách vẫn thực hiện một vòng lặp cấp độ byte:

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

Sử dụng một sự hiểu biết danh sách thay cho một vòng lặp không xây dựng một danh sách, thường tích lũy một cách vô nghĩa một danh sách các giá trị vô nghĩa và sau đó ném danh sách đi, thường là chậm hơn vì quá trình tạo và mở rộng danh sách. Danh sách hiểu không phải là phép thuật vốn đã nhanh hơn một vòng lặp cũ tốt.

Đối với các hàm xử lý danh sách chức năng: Mặc dù các hàm này được viết bằng C và có thể vượt trội hơn các hàm tương đương được viết bằng Python, nhưng chúng không nhất thiết là tùy chọn nhanh nhất. Một số tăng tốc được dự kiến nếu chức năng được viết bằng C quá. Nhưng hầu hết các trường hợp sử dụng một lambda(hoặc chức năng Python khác), chi phí liên tục thiết lập các khung ngăn xếp Python, v.v ... ăn hết mọi khoản tiết kiệm. Đơn giản chỉ cần thực hiện cùng một công việc nội tuyến, không có lệnh gọi hàm (ví dụ: hiểu danh sách thay vì maphoặc filter) thường nhanh hơn một chút.

Giả sử trong một trò chơi mà tôi đang phát triển, tôi cần vẽ các bản đồ phức tạp và khổng lồ bằng cách sử dụng các vòng lặp. Câu hỏi này chắc chắn có liên quan, ví dụ, nếu hiểu toàn bộ danh sách, thực sự nhanh hơn, thì đó sẽ là một lựa chọn tốt hơn nhiều để tránh độ trễ (Mặc dù độ phức tạp về hình ảnh của mã).

Rất có thể, nếu mã như thế này chưa đủ nhanh khi được viết bằng Python không "tối ưu hóa" tốt, thì sẽ không có tối ưu hóa vi mô mức Python nào đủ nhanh và bạn nên bắt đầu nghĩ đến việc giảm xuống C. Trong khi mở rộng tối ưu hóa vi mô thường có thể tăng tốc đáng kể mã Python, có giới hạn thấp (về mặt tuyệt đối) cho điều này. Hơn nữa, ngay cả trước khi bạn chạm trần, nó trở nên đơn giản hơn về chi phí (tăng tốc 15% so với tăng tốc 300% với cùng một nỗ lực) để cắn viên đạn và viết một số C.


25

Nếu bạn kiểm tra thông tin trên python.org , bạn có thể xem tóm tắt này:

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

Nhưng bạn thực sự nên đọc chi tiết bài viết trên để hiểu nguyên nhân của sự khác biệt hiệu suất.

Tôi cũng đề nghị bạn nên tính thời gian cho mã của mình bằng cách sử dụng timeit . Vào cuối ngày, có thể có một tình huống, ví dụ, bạn có thể cần thoát ra khỏi forvòng lặp khi một điều kiện được đáp ứng. Nó có khả năng có thể nhanh hơn tìm ra kết quả bằng cách gọi map.


17
Mặc dù trang đó là một trang đọc tốt và có liên quan một phần, nhưng chỉ trích dẫn những con số đó là không hữu ích, thậm chí có thể gây hiểu nhầm.

1
Điều này không cho biết thời gian của bạn là gì. Hiệu suất tương đối sẽ thay đổi rất nhiều tùy thuộc vào những gì trong vòng lặp / listcomp / map.
user2357112 hỗ trợ Monica

@del Nam Mình đồng ý. Tôi đã sửa đổi câu trả lời của mình để thúc giục OP đọc tài liệu để hiểu sự khác biệt về hiệu suất.
Anthony Kong

@ user2357112 Bạn phải đọc trang wiki tôi đã liên kết cho bối cảnh. Tôi đã đăng nó để tham khảo của OP.
Anthony Kong

13

Bạn hỏi cụ thể về map(), filter()reduce(), nhưng tôi giả sử bạn muốn biết về lập trình chức năng nói chung. Bản thân tôi đã kiểm tra vấn đề về khoảng cách tính toán giữa tất cả các điểm trong một tập hợp các điểm, lập trình chức năng (sử dụng starmapchức năng từ itertoolsmô-đun tích hợp) hóa ra chậm hơn một chút so với các vòng lặp (mất 1,25 lần, trong thực tế). Đây là mã mẫu tôi đã sử dụng:

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

Là phiên bản chức năng nhanh hơn phiên bản thủ tục?

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
Có vẻ như một cách khá phức tạp để trả lời câu hỏi này. Bạn có thể cắt nó xuống để nó có ý nghĩa tốt hơn?
Aaron Hall

2
@AaronHall Tôi thực sự tìm thấy câu trả lời của andreipmbcn khá thú vị bởi vì đây là một ví dụ không tầm thường. Mã chúng ta có thể chơi với.
Anthony Kong

@AaronHall, bạn có muốn tôi chỉnh sửa đoạn văn bản để nó nghe rõ ràng và đơn giản hơn không, hay bạn muốn tôi chỉnh sửa mã?
andreipmbcn

9

Tôi đã viết một kịch bản đơn giản để kiểm tra tốc độ và đây là những gì tôi tìm ra. Trên thực tế cho vòng lặp là nhanh nhất trong trường hợp của tôi. Điều đó thực sự làm tôi ngạc nhiên, kiểm tra dưới đây (đang tính tổng bình phương).

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

Với python 3.6.1 khác biệt không quá lớn; Giảm và Bản đồ đi xuống 0,24 và liệt kê mức độ hiểu thành 0,29. Đối với là cao hơn, tại 0,18.
jjmerelo 18/03/18

Việc loại bỏ phần inttrong square_sum4cũng làm cho nó nhanh hơn một chút và chỉ chậm hơn một chút so với vòng lặp for.
jjmerelo 18/03/18

5

Thêm một thay đổi cho câu trả lời của Alphii , thực sự vòng lặp for sẽ tốt thứ hai và chậm hơn khoảng 6 lần so vớimap

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

Những thay đổi chính là loại bỏ các sumcuộc gọi chậm , cũng như có thể không cần thiết int()trong trường hợp cuối cùng. Đặt vòng lặp for và bản đồ theo cùng một thuật ngữ làm cho nó khá thực tế. Hãy nhớ rằng lambdas là các khái niệm chức năng và về mặt lý thuyết không nên có tác dụng phụ, nhưng, tốt, chúng có thể có tác dụng phụ như thêm vào a. Kết quả trong trường hợp này với 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
vuông_sum3 và vuông_sum4 không chính xác. Họ sẽ không cho tiền. Trả lời dưới đây từ @alisca chen là thực sự chính xác.
ShikharDua

5

Tôi đã sửa đổi mã của @ Alisa và được sử dụng cProfileđể chỉ ra lý do tại sao việc hiểu danh sách nhanh hơn:

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

Đây là kết quả:

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

IMHO:

  • reducemapnói chung là khá chậm. Không chỉ vậy, sử dụng sumtrên các trình vòng lặp maptrả về còn chậm, so với việc sumlấy danh sách
  • for_loop sử dụng phụ lục, tất nhiên là chậm ở một mức độ nào đó
  • hiểu danh sách không chỉ dành ít thời gian nhất để xây dựng danh sách, nó còn làm cho sumnhanh hơn nhiều, ngược lại vớimap

3

Tôi đã quản lý để sửa đổi một số mã của @ alpiii và phát hiện ra rằng việc hiểu danh sách nhanh hơn một chút so với vòng lặp. Nó có thể được gây ra bởi int(), nó không công bằng giữa hiểu danh sách và vòng lặp.

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.