ฟังก์ชั่น list-comprehensions และฟังก์ชั่นการทำงานเร็วกว่า“ for loops” หรือไม่?


155

ในแง่ของประสิทธิภาพในหลามเป็นรายการความเข้าใจหรือฟังก์ชั่นชอบmap(), filter()และreduce()เร็วกว่าสำหรับห่วง? ทำไมในทางเทคนิคแล้วมันทำงานด้วยความเร็ว Cในขณะที่ for loop ทำงานในความเร็วของ python virtual machine ?

สมมติว่าในเกมที่ฉันกำลังพัฒนาฉันต้องวาดแผนที่ที่ซับซ้อนและมีขนาดใหญ่เพื่อใช้เป็นลูป คำถามนี้จะเกี่ยวข้องกันอย่างแน่นอนเช่นหาก list-comprehension เร็วกว่าจริง ๆ มันจะเป็นตัวเลือกที่ดีกว่ามากเพื่อหลีกเลี่ยงความล่าช้า (แม้จะมีความซับซ้อนในการมองเห็นของโค้ด)

คำตอบ:


147

ต่อไปนี้เป็นแนวทางคร่าวๆและการเดาที่มีการศึกษาตามประสบการณ์ คุณควรtimeitหรือโปรไฟล์กรณีการใช้ที่เป็นรูปธรรมของคุณเพื่อให้ได้ตัวเลขที่ยากและตัวเลขเหล่านั้นอาจไม่เห็นด้วยกับด้านล่าง

รายการความเข้าใจมักจะเร็วกว่าการforวนซ้ำที่แม่นยำเพียงเล็กน้อย(ซึ่งจริง ๆ แล้วเป็นการสร้างรายการ) ส่วนใหญ่เนื่องจากไม่จำเป็นต้องค้นหารายการและappendวิธีการในการวนซ้ำทุกครั้ง อย่างไรก็ตามความเข้าใจในรายการยังคงทำวนซ้ำระดับ 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

การใช้ list comprehension แทน loop ที่ไม่ได้สร้าง list โดยการรวบรวมรายการของค่าที่ไม่มีความหมายแล้วทิ้งรายการนั้นไปบ่อยครั้งจะช้ากว่าเนื่องจากการสร้างและขยายรายการ ความเข้าใจในรายการไม่ใช่เวทมนตร์ที่โดยธรรมชาติเร็วกว่าลูปเก่าที่ดี

สำหรับฟังก์ชั่นการประมวลผลรายการฟังก์ชั่น: ในขณะที่เหล่านี้ถูกเขียนใน C และอาจมีประสิทธิภาพเทียบเท่าฟังก์ชั่นเทียบเท่าเขียนใน Python พวกเขาไม่จำเป็นต้องเป็นตัวเลือกที่เร็วที่สุด คาดว่าจะเพิ่มความเร็วขึ้นถ้าฟังก์ชั่นเขียนด้วยภาษา C เช่นกัน แต่กรณีส่วนใหญ่ที่ใช้lambda(หรือฟังก์ชั่น Python อื่น ๆ ) ค่าโสหุ้ยของการตั้งค่า Python stack frames ซ้ำ ๆ เป็นต้นจะช่วยประหยัดค่าใช้จ่ายใด ๆ เพียงทำงานในบรรทัดเดียวกันโดยไม่ต้องเรียกใช้ฟังก์ชัน (เช่นความเข้าใจในรายการแทนmapหรือfilter) มักเร็วกว่าเล็กน้อย

สมมติว่าในเกมที่ฉันกำลังพัฒนาฉันต้องวาดแผนที่ที่ซับซ้อนและมีขนาดใหญ่เพื่อใช้เป็นลูป คำถามนี้จะเกี่ยวข้องกันอย่างแน่นอนเช่นหาก list-comprehension เร็วกว่าจริง ๆ มันจะเป็นตัวเลือกที่ดีกว่ามากเพื่อหลีกเลี่ยงความล่าช้า (แม้จะมีความซับซ้อนในการมองเห็นของโค้ด)

โอกาสคือถ้ารหัสเช่นนี้ยังไม่เร็วพอเมื่อเขียนด้วย Python ที่ไม่ได้“ ปรับให้เหมาะสม” จะไม่มีการเพิ่มประสิทธิภาพระดับจุลภาคของ Python จำนวนใดที่จะทำให้เร็วพอและคุณควรเริ่มคิดที่จะทิ้ง C ในขณะที่กว้างขวาง การเพิ่มประสิทธิภาพขนาดเล็กมักจะทำให้โค้ด Python เร็วขึ้นมีขีด จำกัด ต่ำ (ในแง่แน่นอน) ยิ่งกว่านั้นก่อนที่คุณจะไปถึงเพดานนั้นมันก็จะคุ้มค่ามากขึ้น (เร็วขึ้น 15% เทียบกับความเร็ว 300% ด้วยความพยายามเดียวกัน) เพื่อกัดกระสุนและเขียน C บางส่วน


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 ในตอนท้ายของวันอาจมีสถานการณ์ที่ตัวอย่างเช่นคุณอาจต้องแยกจากforลูปเมื่อตรงตามเงื่อนไข mapมันอาจจะเร็วกว่าการหาผลโดยการโทร


17
ในขณะที่หน้านั้นเป็นการอ่านที่ดีและมีความเกี่ยวข้องเพียงบางส่วนการอ้างอิงตัวเลขเหล่านั้นไม่เป็นประโยชน์ แต่อาจทำให้เข้าใจผิดได้

1
สิ่งนี้ไม่ได้บ่งบอกสิ่งที่คุณต้องการ ประสิทธิภาพสัมพัทธ์จะแตกต่างกันอย่างมากขึ้นอยู่กับสิ่งที่อยู่ในลูป / listcomp / แผนที่
user2357112 รองรับ Monica

@delnan ฉันเห็นด้วย ฉันได้แก้ไขคำตอบของฉันเพื่อกระตุ้น OP เพื่ออ่านเอกสารประกอบเพื่อทำความเข้าใจความแตกต่างของประสิทธิภาพ
Anthony Kong

@ user2357112 คุณต้องอ่านหน้าวิกิที่ฉันเชื่อมโยงกับบริบท ฉันโพสต์ไว้เพื่ออ้างอิงของ OP
Anthony Kong

13

คุณถามเฉพาะเกี่ยวกับmap(), filter()และreduce()แต่ผมถือว่าคุณอยากรู้เกี่ยวกับการเขียนโปรแกรมการทำงานโดยทั่วไป เมื่อทดสอบตัวเองเกี่ยวกับปัญหาการคำนวณระยะทางระหว่างทุกจุดภายในชุดของจุดการเขียนโปรแกรมการใช้งาน (โดยใช้starmapฟังก์ชั่นจากitertoolsโมดูลในตัว) กลับกลายเป็นช้ากว่า for-loops เล็กน้อย (ใช้เวลา 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
ดูเหมือนจะเป็นวิธีที่ค่อนข้างซับซ้อนในการตอบคำถามนี้ คุณสามารถตัดมันลงเพื่อให้เหมาะสมได้หรือไม่?
Aaron Hall

2
@AaronHall จริง ๆ แล้วฉันพบคำตอบของ andreipmbcn ค่อนข้างน่าสนใจเพราะมันเป็นตัวอย่างที่ไม่สำคัญ รหัสที่เราสามารถเล่นได้
Anthony Kong

@AaronHall คุณต้องการให้ฉันแก้ไขย่อหน้าข้อความเพื่อให้ชัดเจนและตรงไปตรงมายิ่งขึ้นหรือคุณต้องการให้ฉันแก้ไขรหัสหรือไม่
andreipmbcn

9

ฉันเขียนสคริปต์ง่าย ๆ ที่ทดสอบความเร็วและนี่คือสิ่งที่ฉันค้นพบ ที่จริงแล้วการวนซ้ำนั้นเร็วที่สุดในกรณีของฉัน นั่นทำให้ฉันประหลาดใจมากเลยลองตะโกน (คำนวณผลรวมของกำลังสอง)

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

การกำจัดสิ่งที่intอยู่ข้างในsquare_sum4ทำให้มันค่อนข้างเร็วกว่าและช้ากว่านิดหน่อยสำหรับลูป
jjmerelo

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}

IMHO:

  • reduceและmapโดยทั่วไปแล้วค่อนข้างช้า ไม่เพียงแค่นั้นการใช้งานsumตัววนซ้ำที่mapส่งคืนจะช้าเมื่อเปรียบเทียบกับsumรายการ
  • for_loop ใช้ผนวกซึ่งแน่นอนว่าช้าไปบ้าง
  • list-comprehension ไม่เพียง แต่ใช้เวลาน้อยที่สุดในการสร้างรายการ แต่ยังทำให้sumเร็วขึ้นมากเมื่อเทียบกับmap

5

การเพิ่มคำตอบลงใน Alphiiจริง ๆ แล้วสำหรับ for loop จะดีที่สุดเป็นอันดับสองและช้ากว่าประมาณ 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 for loop และ map ในคำเดียวกันทำให้ค่อนข้างเป็นจริง โปรดจำไว้ว่า lambdas เป็นแนวคิดในการทำงานและในทางทฤษฎีไม่ควรมีผลข้างเคียง แต่ก็สามารถมีผลข้างเคียงเช่นการเพิ่มเข้าไป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บางส่วนและค้นพบว่า List comprehension นั้นเร็วกว่าลูปเล็กน้อย มันอาจเกิดจากint()มันไม่ยุติธรรมระหว่างความเข้าใจในรายการและสำหรับวง

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.