在今天的Google Code Jam(特别是' The Repeater')中,我在两段代码之间遇到了以下奇怪的性能差异。
对于下面的snippits,假设lengths
是正整数列表。
当使用以下代码(嵌套在循环中的某个地方)时,我的解决方案大约需要3.1秒。
minmov = -1
for target in range(101):
mov = 0
for l in lengths:
mov += abs(l - target)
if mov < minmov or minmov == -1:
minmov = mov
moves += minmov
然而,当我用下面看到的功能相当的snippit替换它时,它突然需要4.2秒。飙升33%!
moves += min(sum(abs(l - t) for l in lengths) for t in range(101))
任何人都可以向我解释为什么这么慢?对于不经意的观察者来说,为什么这会做出不同的事情并不是很明显。
答案 0 :(得分:1)
您可以使用Python的标准lib cProfile
模块来查看代码中消耗时间的位置。我用最小的工作示例包装了代码并对其进行了分析(不确定它是否完全模仿算法的行为,但我希望它对我的观点有用):
import random
moves = 0
lengths = [random.randint(-10,10) for _ in range(101)]
def foo():
global moves
minmov = -1
for target in range(101):
mov = 0
for l in lengths:
mov += abs(l - target)
if mov < minmov or minmov == -1:
minmov = mov
moves += minmov
def foo2():
global moves
moves += min(sum(abs(l - t) for l in lengths) for t in range(101))
import cProfile
cProfile.run("foo2()")
#cProfile.run("foo2()")
python t.py
10205 function calls in 0.023 CPU seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.023 0.023 <string>:1(<module>)
1 0.013 0.013 0.023 0.023 t.py:5(foo)
10201 0.010 0.000 0.010 0.000 {abs}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1 0.000 0.000 0.000 0.000 {range}
python t1.py
10409 function calls in 0.023 CPU seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.047 0.047 <string>:1(<module>)
1 0.000 0.000 0.047 0.047 t.py:16(foo2)
102 0.000 0.000 0.047 0.000 t.py:18(<genexpr>)
10201 0.010 0.000 0.010 0.000 {abs}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1 0.000 0.000 0.047 0.047 {min}
1 0.000 0.000 0.000 0.000 {range}
101 0.012 0.000 0.046 0.000 {sum}
在我的电脑(Windows,Python2.7)上,它们几乎以相同的速度运行,但是注意功能代码还有多少函数调用。 Python中的函数调用很昂贵,有时最好坚持使用循环和简单的解决方案。
通过查看配置文件转储,sum
正在调用101次,在迭代解决方案中,您可以使用+=
运算符执行此操作,因此不会调用任何函数。
功能代码将循环移动到C,但这样做的代价为101
(或者这101只意味着lengths
的长度,所以它将以{{1}为代价。 }})更多函数调用。
我敢打赌这是造成额外开销的原因。您可以阅读Guido的优秀Python Patterns - An Optimization Anecdote文章,以获得有关此主题的更多信息。
我不完全确定len(lengths)
必须再次迭代整个数组以提取注释中提到的最小值。功能解决方案基于发电机。在这种情况下,外部发电机min
是移动发电机所有机械的发动机。
当min
启动时,它将尝试查找作为参数传递的生成器上的第一个元素,这反过来将唤醒min
方法并启动sum的参数生成器,产生{{1} }作为sum
的参数生成器的第一个值,这样做直到计算出整个总和abs(l[0] - 0)
,那么这将是sum
的参数生成器的第一个值。当abs(l[0]-0) + abs(l[1]-0)...
移动以检查其参数生成器中的第二个元素时,min
将跟踪它。等等。
隐含地,min
实际上是跟踪最小值,因为您正在使用生成器而非列表推导。
希望这有帮助!