关于python:列表推导和功能函数是否比“for loops”更快?

Are list-comprehensions and functional functions faster than “for loops”?

就Python的性能而言,列表理解或map()、filter()和reduce()等函数是否比for循环更快?为什么在技术上,它们"以C速度运行",而"for循环以python虚拟机速度运行"?.

假设在我正在开发的一个游戏中,我需要使用for循环绘制复杂和巨大的地图。这个问题肯定是相关的,例如,如果列表理解速度确实更快,那么为了避免滞后(尽管代码的视觉复杂性),它将是一个更好的选择。


以下是基于经验的粗略指导和有根据的猜测。您应该使用cx1(2)或概要介绍您的具体用例以获得硬数字,这些数字有时可能与下面的数字不一致。

列表理解通常比精确等效的for循环(实际上是构建一个列表)快一点,这很可能是因为它不必在每次迭代中查找列表及其append方法。但是,列表理解仍然执行字节码级循环:

1
2
3
4
5
6
7
8
9
>>> 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编写的,则会有一些加速。但大多数情况下,使用EDOCX1(或其他python函数),重复设置python堆栈帧等的开销会消耗掉任何节省。在没有函数调用的情况下(例如,列表理解而不是mapfilter)直接执行相同的工作通常速度稍快。

Suppose that in a game that I'm developing I need to draw complex and huge maps using for loops. This question would be definitely relevant, for if a list-comprehension, for example, is indeed faster, it would be a much better option in order to avoid lags (Despite the visual complexity of the code).

如果像这样的代码在用良好的非"优化"python编写时速度不够快,那么没有多少python级别的微优化能够使其足够快,您应该开始考虑降低到C级。虽然广泛的微优化通常可以大大加快python代码的速度,但有一个较低的(绝对的)lim就这样。此外,即使在你达到这个上限之前,咬一口子弹并写一些C也会变得更具成本效益(15%的加速比300%的加速同样的努力)。


如果您查看python.org上的信息,可以看到以下摘要:

1
2
3
4
5
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发现结果更快。


您特别询问map()、filter()和reduce(),但我假设您希望了解一般的函数编程。在对一组点内所有点之间的距离计算问题进行了自我测试后,函数编程(使用内置ITertools模块中的Starmap函数)结果比循环的速度稍慢(事实上,耗时是循环的1.25倍)。这是我使用的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

功能版本比过程版本快吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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')


我写了一个简单的脚本来测试速度,这就是我发现的。实际上,在我的例子中for循环是最快的。这真让我吃惊,看看贝罗(正在计算平方和)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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])

埃多克斯1〔8〕


在alphii答案中加上一个转数,for循环实际上是次优的,比map慢6倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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循环和map放在相同的术语中会使其非常真实。记住,lambda是功能概念,理论上不应该有副作用,但是,好吧,它们可以有副作用,比如添加到a中。在这种情况下,使用python 3.6.1、ubuntu 14.04、intel(r)core(tm)i7-4770 [email protected]时的结果是

1
2
3
4
0:00:00.257703
0:00:00.184898
0:00:00.031718
0:00:00.212699