优化:循环与求和生成器理解

时间:2014-05-03 21:35:23

标签: python optimization

在今天的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))

任何人都可以向我解释为什么这么慢?对于不经意的观察者来说,为什么这会做出不同的事情并不是很明显。

1 个答案:

答案 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实际上是跟踪最小值,因为您正在使用生成器而非列表推导

希望这有帮助!