itertools.product 比嵌套 for 循环慢
Posted
技术标签:
【中文标题】itertools.product 比嵌套 for 循环慢【英文标题】:itertools.product slower than nested for loops 【发布时间】:2014-07-03 13:46:39 【问题描述】:我正在尝试使用itertools.product
函数使我的一段代码(在同位素模式模拟器中)更易于阅读并且希望也更快(documentation 声明没有创建中间结果),我有然而,使用 cProfiling
库对两个版本的代码进行了相互测试,发现 itertools.product
比我的嵌套 for 循环慢得多。
用于测试的示例值:
carbons = [(0.0, 0.004613223957020534), (1.00335, 0.02494768843632857), (2.0067, 0.0673219412049374), (3.0100499999999997, 0.12087054681917497), (4.0134, 0.16243239687902825), (5.01675, 0.17427700732161705), (6.020099999999999, 0.15550695260604208), (7.0234499999999995, 0.11869556397525197), (8.0268, 0.07911287899598853), (9.030149999999999, 0.04677626606764402)]
hydrogens = [(0.0, 0.9417611429667746), (1.00628, 0.05651245007201512)]
nitrogens = [(0.0, 0.16148864310897554), (0.99703, 0.2949830688288726), (1.99406, 0.26887643366755537), (2.99109, 0.16305943261399866), (3.98812, 0.0740163089529218), (4.98515, 0.026824040474519875), (5.98218, 0.008084687617425748)]
oxygens17 = [(0.0, 0.8269292736927519), (1.00422, 0.15717628899143962), (2.00844, 0.014907548827832968)]
oxygens18 = [(0.0, 0.3584191873916266), (2.00425, 0.36813434247849824), (4.0085, 0.18867830334103902), (6.01275, 0.06433912182670033), (8.017, 0.016421642936302827)]
sulfurs33 = [(0.0, 0.02204843659673093), (0.99939, 0.08442569434459646), (1.99878, 0.16131398792444965), (2.99817, 0.2050722764666321), (3.99756, 0.1951327596407101), (4.99695, 0.14824112268069747), (5.99634, 0.09365899226198841), (6.99573, 0.050618028523695714), (7.99512, 0.023888506307006133), (8.99451, 0.010000884811585533)]
sulfurs34 = [(0.0, 3.0106350597190195e-10), (1.9958, 6.747270089956428e-09), (3.9916, 7.54568412614702e-08), (5.9874, 5.614443102700176e-07), (7.9832, 3.1268212758750728e-06), (9.979, 1.3903197959791067e-05), (11.9748, 5.141248916434075e-05), (13.970600000000001, 0.0001626288218672788), (15.9664, 0.00044921518047309414), (17.9622, 0.0011007203440032396)]
sulfurs36 = [(0.0, 0.904828368500412), (3.99501, 0.0905009370374487)]
演示嵌套 for 循环的片段:
totals = []
for i in carbons:
for j in hydrogens:
for k in nitrogens:
for l in oxygens17:
for m in oxygens18:
for n in sulfurs33:
for o in sulfurs34:
for p in sulfurs36:
totals.append((i[0]+j[0]+k[0]+l[0]+m[0]+n[0]+o[0]+p[0], i[1]*j[1]*k[1]*l[1]*m[1]*n[1]*o[1]*p[1]))
片段演示了itertools.product
的用法:
totals = []
for i in itertools.product(carbons,hydrogens,nitrogens,oxygens17,oxygens18,sulfurs33,sulfurs34,sulfurs36):
massDiff = i[0][0]
chance = i[0][1]
for j in i[1:]:
massDiff += j[0]
chance = chance * j[1]
totals.append((massDiff,chance))
分析的结果(基于每个方法 10 次运行)对于嵌套 for 循环方法平均约为 0.8 秒,对于 itertools.product
方法平均约为 1.3 秒。因此,我的问题是,我是错误地使用了 itertools.product
函数还是应该坚持使用嵌套的 for 循环?
-- 更新--
我已经包含了我的两个cProfile
结果:
# ITERTOOLS.PRODUCT APPROACH
420003 function calls in 1.306 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.018 0.018 1.306 1.306 <string>:1(<module>)
1 1.246 1.246 1.289 1.289 IsotopeBas.py:64(option1)
420000 0.042 0.000 0.042 0.000 method 'append' of 'list' objects
1 0.000 0.000 0.000 0.000 method 'disable' of '_lsprof.Profiler' objects
和:
# NESTED FOR LOOP APPROACH
420003 function calls in 0.830 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.019 0.019 0.830 0.830 <string>:1(<module>)
1 0.769 0.769 0.811 0.811 IsotopeBas.py:78(option2)
420000 0.042 0.000 0.042 0.000 method 'append' of 'list' objects
1 0.000 0.000 0.000 0.000 method 'disable' of '_lsprof.Profiler' objects
【问题讨论】:
能否给carbons,hydrogens,nitrogens,oxygens17,oxygens18,sulfurs33,sulfurs34,sulfurs36
一些样本值,以便我们重现并确认
这不是您在 Python 中计时代码速度的方式。使用docs.python.org/3.4/library/timeit.html
我会用timeit
重试它,我会在 OP 中添加一些示例列表(为大尺寸道歉)。
您正在使用 append 来创造机会,并重新运行循环减少,即使您可以在 for 等中乘以,等等。
我确实做到了。我已经调整了 itertools sn-p 以更正您所描述的内容,但是它仍然较慢。
【参考方案1】:
您最初的 itertool 代码在不必要的 lambda
上花费了很多额外的时间,并手动构建中间值列表 - 其中很多都可以用内置功能替换。
现在,内部 for 循环确实增加了很多额外的开销:只需尝试以下操作,性能与您的原始代码非常接近:
for a in itertools.product(carbons,hydrogens,nitrogens,oxygens17,
oxygens18,sulfurs33,sulfurs34,sulfurs36):
i, j, k, l, m, n, o, p = a
totals.append((i[0]+j[0]+k[0]+l[0]+m[0]+n[0]+o[0]+p[0],
i[1]*j[1]*k[1]*l[1]*m[1]*n[1]*o[1]*p[1]))
以下代码尽可能在CPython内置端运行,我测试它与with code等价。值得注意的是,该代码使用zip(*iterable)
来解压缩每个产品结果;然后将reduce
与operator.mul
用于乘积,sum
用于求和; 2 个用于遍历列表的生成器。 for 循环仍然有轻微的跳动,但是从长远来看,它可能不是您可以使用的硬编码。
import itertools
from operator import mul
from functools import partial
prod = partial(reduce, mul)
elems = carbons, hydrogens, nitrogens, oxygens17, oxygens18, sulfurs33, sulfurs34, sulfurs36
p = itertools.product(*elems)
totals = [
( sum(massdiffs), prod(chances) )
for massdiffs, chances in
( zip(*i) for i in p )
]
【讨论】:
我已经删除了 lambda 块(这有点傻,我不得不同意)。你会说(在我现在的 sn-ps 中)剩余的差异(大约 0.5 秒)来自中间值吗? 我的代码给出了 0.788 vs 0.562 的循环,所以我猜不会快多少 差异已经比我最初观察到的要小很多,但我仍然对itertools.product
函数在性能上的显着差异感到惊讶。
我不得不说,我对于指出仍然使itertools
变体比嵌套 for 循环慢的原因没有进一步的想法,因为 reduce 已从代码中删除,唯一的区别根据cProfiler
来自分析输出中的第二行(这并没有说明太多)。
我真的很喜欢你如何摆脱我在其中的额外 for 循环,而且确实这种实现使性能差异微不足道(在两个变体之间),干杯。【参考方案2】:
我对这两个函数进行了计时,它们使用了绝对最少的额外代码:
def nested_for(first_iter, second_iter):
for i in first_iter:
for j in second_iter:
pass
def using_product(first_iter, second_iter):
for i in product(first_iter, second_iter):
pass
它们的字节码指令类似:
dis(nested_for)
2 0 SETUP_LOOP 26 (to 28)
2 LOAD_FAST 0 (first_iter)
4 GET_ITER
>> 6 FOR_ITER 18 (to 26)
8 STORE_FAST 2 (i)
3 10 SETUP_LOOP 12 (to 24)
12 LOAD_FAST 1 (second_iter)
14 GET_ITER
>> 16 FOR_ITER 4 (to 22)
18 STORE_FAST 3 (j)
4 20 JUMP_ABSOLUTE 16
>> 22 POP_BLOCK
>> 24 JUMP_ABSOLUTE 6
>> 26 POP_BLOCK
>> 28 LOAD_CONST 0 (None)
30 RETURN_VALUE
dis(using_product)
2 0 SETUP_LOOP 18 (to 20)
2 LOAD_GLOBAL 0 (product)
4 LOAD_FAST 0 (first_iter)
6 LOAD_FAST 1 (second_iter)
8 CALL_FUNCTION 2
10 GET_ITER
>> 12 FOR_ITER 4 (to 18)
14 STORE_FAST 2 (i)
3 16 JUMP_ABSOLUTE 12
>> 18 POP_BLOCK
>> 20 LOAD_CONST 0 (None)
22 RETURN_VALUE
结果如下:
>>> timer = partial(timeit, number=1000, globals=globals())
>>> timer("nested_for(range(100), range(100))")
0.1294467518782625
>>> timer("using_product(range(100), range(100))")
0.4335527486212385
通过timeit
和手动使用perf_counter
进行的额外测试结果与上述一致。使用product
显然比使用嵌套的for
循环慢很多。但是,根据先前答案中已经显示的测试,这两种方法之间的差异与嵌套循环的数量(当然还有包含笛卡尔积的元组的大小)成反比。
【讨论】:
【参考方案3】:我强烈怀疑缓慢来自于每次通过lambda
创建临时变量/在地方添加/创建函数以及函数调用的开销。只是为了说明为什么在案例 2 中你做加法的速度较慢:
import dis
s = '''
a = (1, 2)
b = (2, 3)
c = (3, 4)
z = (a[0] + b[0] + c[0])
t = 0
t += a[0]
t += b[0]
t += c[0]
'''
x = compile(s, '', 'exec')
dis.dis(x)
这给出了:
<snip out variable declaration>
5 18 LOAD_NAME 0 (a)
21 LOAD_CONST 4 (0)
24 BINARY_SUBSCR
25 LOAD_NAME 1 (b)
28 LOAD_CONST 4 (0)
31 BINARY_SUBSCR
32 BINARY_ADD
33 LOAD_NAME 2 (c)
36 LOAD_CONST 4 (0)
39 BINARY_SUBSCR
40 BINARY_ADD
41 STORE_NAME 3 (z)
7 50 LOAD_NAME 4 (t)
53 LOAD_NAME 0 (a)
56 LOAD_CONST 4 (0)
59 BINARY_SUBSCR
60 INPLACE_ADD
61 STORE_NAME 4 (t)
8 64 LOAD_NAME 4 (t)
67 LOAD_NAME 1 (b)
70 LOAD_CONST 4 (0)
73 BINARY_SUBSCR
74 INPLACE_ADD
75 STORE_NAME 4 (t)
9 78 LOAD_NAME 4 (t)
81 LOAD_NAME 2 (c)
84 LOAD_CONST 4 (0)
87 BINARY_SUBSCR
88 INPLACE_ADD
89 STORE_NAME 4 (t)
92 LOAD_CONST 5 (None)
95 RETURN_VALUE
正如您所见,由于 +=
加法与内联加法,额外的 2 个操作码开销。这种开销来自需要加载和存储名称。我想这只是开始,Antti Haapala 的代码在调用 c 代码的 cpython 内置函数中花费的时间比仅在 python 中运行的时间更多。 python中的函数调用开销很大。
【讨论】:
以上是关于itertools.product 比嵌套 for 循环慢的主要内容,如果未能解决你的问题,请参考以下文章
算法:用itertools.product()简化嵌套for循环
itertools.product(np.arange(0.0, 1.1, 0.1), repeat=30) 杀死进程