我的理解是你并不真的需要让代码更快,你只是想知道为什么它更慢。为了回答这个问题,让我们看一下反汇编。出于本次讨论的目的,我将在函数调用中包装每个方法,在每次反汇编中可以忽略 and 的加载和返回命令u
。v
def test1(u, v):
return (u[0]-v[0])**2 + (u[1]-v[1])**2 + (u[3]-v[3])**2
dis.dis(test1)
0 LOAD_FAST 0 (u)
3 LOAD_CONST 1 (0)
6 BINARY_SUBSCR
7 LOAD_FAST 1 (v)
10 LOAD_CONST 1 (0)
13 BINARY_SUBSCR
14 BINARY_SUBTRACT
15 LOAD_CONST 2 (2)
18 BINARY_POWER
19 LOAD_FAST 0 (u)
22 LOAD_CONST 3 (1)
25 BINARY_SUBSCR
26 LOAD_FAST 1 (v)
29 LOAD_CONST 3 (1)
32 BINARY_SUBSCR
33 BINARY_SUBTRACT
34 LOAD_CONST 2 (2)
37 BINARY_POWER
38 BINARY_ADD
39 LOAD_FAST 0 (u)
42 LOAD_CONST 4 (3)
45 BINARY_SUBSCR
46 LOAD_FAST 1 (v)
49 LOAD_CONST 4 (3)
52 BINARY_SUBSCR
53 BINARY_SUBTRACT
54 LOAD_CONST 2 (2)
57 BINARY_POWER
58 BINARY_ADD
59 RETURN_VALUE
我将第一个示例剪掉了 3 的长度,因为它只会一遍又一遍地重复相同的模式。您可以很快看到没有函数调用开销,并且解释器几乎在这些操作数上做了尽可能少的工作来实现您的结果。
def test2(u, v):
sum((a-b)**2 for a, b in izip(u, v))
dis.dis(test2)
0 LOAD_GLOBAL 0 (sum)
3 LOAD_CONST 1 (<code object <genexpr> at 02C6F458, file "<pyshell#10>", line 2>)
6 MAKE_FUNCTION 0
9 LOAD_GLOBAL 1 (izip)
12 LOAD_FAST 0 (u)
15 LOAD_FAST 1 (v)
18 CALL_FUNCTION 2
21 GET_ITER
22 CALL_FUNCTION 1
25 CALL_FUNCTION 1
28 RETURN_VALUE
我们在这里看到的是,我们从生成器表达式中创建了一个函数,加载了 2 个全局变量(sum 和 izip,全局查找比本地查找慢,我们无法避免只创建一次,但如果要调用它们一个紧密的循环,许多人将它们分配给一个本地,例如_izip
or _sum
),然后连续遭受 4 次昂贵的字节码操作,调用 izip,从生成器中获取迭代器,调用生成器创建的函数,然后调用sum 函数(它将消耗迭代器并在返回之前添加每个项目)。
def test3(u, v):
sum = 0
for i in xrange(len(u)):
sum += (u[i]-v[i])**2
dis.dis(test3)
0 LOAD_CONST 1 (0)
3 STORE_FAST 2 (sum)
6 SETUP_LOOP 52 (to 61)
9 LOAD_GLOBAL 0 (xrange)
12 LOAD_GLOBAL 1 (len)
15 LOAD_FAST 0 (u)
18 CALL_FUNCTION 1
21 CALL_FUNCTION 1
24 GET_ITER
25 FOR_ITER 32 (to 60)
28 STORE_FAST 3 (i)
31 LOAD_FAST 2 (sum)
34 LOAD_FAST 0 (u)
37 LOAD_FAST 3 (i)
40 BINARY_SUBSCR
41 LOAD_FAST 1 (v)
44 LOAD_FAST 3 (i)
47 BINARY_SUBSCR
48 BINARY_SUBTRACT
49 LOAD_CONST 2 (2)
52 BINARY_POWER
53 INPLACE_ADD
54 STORE_FAST 2 (sum)
57 JUMP_ABSOLUTE 25
60 POP_BLOCK
61 LOAD_CONST 0 (None)
64 RETURN_VALUE
我们在这里看到的是test2
. 没有生成器表达式或求和调用,但我们已经用不必要的函数调用替换了函数调用开销,xrange(len(u))
而不是 @Lucas Malor 建议的更快的解决方案。
def test4(u, v):
mysum = 0
for a, b in izip(u, v) :
mysum += (a-b)**2
return mysum
dis.dis(test4)
0 LOAD_CONST 1 (0)
3 STORE_FAST 2 (mysum)
6 SETUP_LOOP 47 (to 56)
9 LOAD_GLOBAL 0 (izip)
12 LOAD_FAST 0 (u)
15 LOAD_FAST 1 (v)
18 CALL_FUNCTION 2
21 GET_ITER
22 FOR_ITER 30 (to 55)
25 UNPACK_SEQUENCE 2
28 STORE_FAST 3 (a)
31 STORE_FAST 4 (b)
34 LOAD_FAST 2 (mysum)
37 LOAD_FAST 3 (a)
40 LOAD_FAST 4 (b)
43 BINARY_SUBTRACT
44 LOAD_CONST 2 (2)
47 BINARY_POWER
48 INPLACE_ADD
49 STORE_FAST 2 (mysum)
52 JUMP_ABSOLUTE 22
55 POP_BLOCK
56 LOAD_FAST 2 (mysum)
59 RETURN_VALUE
以上代表@Lucas Malor 的贡献,它在某些方面更快。它用解包替换下标操作,同时将调用次数减少到 1。在许多情况下,这与您在给我们的约束条件下实现的速度一样快。
请注意,如果您要调用该函数的次数足够多以承受开销,那么只值得eval
使用类似于 test1 中的函数的运行时生成的字符串。另请注意,随着 u 和 v 的长度变得越来越大(这通常是评估此类算法的方式),其他解决方案的函数调用开销变得越来越小,因此,在大多数情况下,最易读的解决方案要优越得多. 同时,即使在小情况下速度较慢,但如果序列 u 和 v 的长度可能很长,我建议使用生成器表达式而不是列表推导式。在大多数情况下,内存节省将导致更快的执行(和更快的 gc)。
总的来说,我的建议是,在短序列情况下的微小加速不值得增加代码大小和与您通过执行微优化查看的其他 python 实现不一致的行为。“最佳”解决方案几乎可以肯定是test2
.