【问题标题】:Converting while to generator 3.4 times slow down将 while 转换为生成器速度减慢 3.4 倍
【发布时间】:2011-04-23 14:01:10
【问题描述】:

发生了什么?有人可以解释一下这里发生了什么吗,我在紧密循环中改变了:

##            j=i
##            while j < ls - 1 and len(wordlist[j]) > lc: j+=1
            j = next(j for j in range(i,ls) if len(wordlist[j]) <=  lc)

注释的版本运行整个程序:625 ms,下一个生成器版本时间运行整个程序2.125 秒

这个更 Python 的版本导致性能如此灾难性的原因是什么?

编辑:可能是因为使用了 psyco 模块?当然,至少没有 psyco 的 Python 2.7 的运行时间是下一个版本的 2.141,这意味着与带有 psyco 的 Python 2.6 几乎相同。

删除 *.pyc 文件后,我没有得到放慢速度的代码。然后,当我也从库模块中删除了 psyco 的导入时,我得到了 2.6 的时间,也可以在没有 psyco 的情况下使用,非 psyco 版本和 psyco 版本的结果(因为现在库程序也变慢了,它的时间也相关:)

不是心理医生:

  1. 同时:图书馆准备:532 ms,总运行时间 2.625 s
  2. 下一步:在库中准备:532 毫秒, 总运行时间(time.clock()): 2.844 s(xrange 相同挂时的版本)

心理:

  1. 同时:图书馆准备:297 ms,总运行时间:609..675 ms
  2. 下一步:在库中准备:297 毫秒, 总运行时间: 1.922 秒(程序中到处都有范围而不是 xrange 的版本:1.985 秒)

在具有 2GB RAM 的 WindowsXP AMD Sempron 3100+ 系统中运行。用两个全局变量计算循环和调用:

    j=i
    callcount += 1
    while j < ls - 1 and len(wordlist[j]) > lc:
        j+=1
        loopcount += 1

使用 psyco 测试输入的结果:

Finished in 625 ms
Loopcount: 78317
Callcount: 47970
Ration: 1.633

所以循环在紧密循环内,但平均只执行几次(请注意,全局计数器的两次增量并没有减慢 psyco 中的代码)

结论: 尽管算法相对于词汇长度的高度敏感,这导致我通过这个循环考虑了一些不可能的单词,但后来递归的基本情况通过字典查找检查,即 O(n),因此 非常有益的早期优化变得不是很有用,即使输入更长并且在函数开头移动调用计数计数器,表明调用计数不受词汇长度的影响,但外部循环计数被迅速减少(最初发布的代码在 if 语句的 elif 部分)。

更长的运行时间(29 372 个解决方案),同时删除了 while 循环和整个循环(使用 i 而不是 j)(库准备 312 毫秒):

  1. 无循环:elif 分支计数:485488,outerloopcount:10129147,比率:0,048,运行时间 6,000 秒(无计数器:4,594 秒
  2. 有循环:循环计数:19355114,外部计数:8194033,比率:0,236,运行时间 5,704 秒(无计数器:4,688 秒

(没有循环、计数器和 psyco 的运行时间:32,792 秒,库 608 毫秒)

因此,如果没有额外的计数器,使用 psyco 的这个循环的好处在更难的情况下:(4688-4594)*100/4688.0 % = 2 %

这启发了我扭转另一个早期的优化,这是我在 DaniWeb 中一直想知道的。早期版本的代码运行得更快,当最小的字长是全局的,而不是参数。根据文档,局部变量调用更快,但显然使递归更重的成本超过了这一点。现在在更困难的情况下,这种优化的另一种逆转带来了更多在没有优化字长的情况下的预期性能行为:使用 psyco 的运行时间是 312 毫秒准备,4,469..4,484 秒总运行时间。因此,这使代码更清晰,并在这种情况下带来了更多好处,就像删除的循环一样。并且用while循环将参数放到版本中,运行时间并没有太大变化(库准备代码的变化变得更大)

**What I learned from this: If you do n optimizations for speed 
you must check the first n-1 optimizations after doing nth one**

【问题讨论】:

  • 如果你真的想比较,你应该使用xrange()
  • 没有 psyco 会发生这种情况吗?
  • 第二个版本对我来说似乎并不像pythonic。
  • 我无法重现您所看到的内容,尽管我不得不猜测lslcwordlist 是什么以及您运行代码的频率.即便如此,Amber 对xrange() 的建议完全有可能解决了这个问题。 (Amber 应该将其作为答案发布。)如果没有,您应该提供有关代码如何运行的更多信息。
  • 根据 musicfreak 对我的回答的评论,您能否在禁用 Psyco 的情况下对此进行基准测试?此外,循环内的代码有多丰富(我们在这里谈论多少次迭代)?随着迭代次数的增加,JIT 编译将倾向于提高性能。

标签: python optimization performance generator while-loop


【解决方案1】:

我发现使用生成器通常比生成整个列表要慢,这有点违反直觉。我已经设法通过添加 [] 对来解决性能瓶颈。

例如比较这些:

$ python -m timeit -n 1000 "' '.join(c for c in 'hello world')"
1000 loops, best of 3: 6.11 usec per loop
$ python -m timeit -n 1000 "' '.join([c for c in 'hello world'])"
1000 loops, best of 3: 3.79 usec per loop

与使用生成器相比,先生成整个列表几乎快两倍!

编辑: 正如 Thomas Wouters 在 cmets 中指出的那样,这里的生成器速度较慢的原因是因为这是一个非常简单的案例。为了平衡,这里是他的测试,其中生成器是明显的赢家:

$ python -m timeit -s "s = 'hello world' * 10000" -s "class C: pass" "for i in (C() for c in s): pass"
10 loops, best of 3: 33.6 msec per loop
$ python -m timeit -s "s = 'hello world' * 10000" -s "class C: pass" "for i in [C() for c in s]: pass"
10 loops, best of 3: 172 msec per loop

【讨论】:

  • 是的,生成器必须为每个元素做一些额外的工作,而不是创建一个列表然后进行迭代。但是,这是否足以引起注意在很大程度上取决于完整列表在内存中的适合程度(仅通过查看代码并不容易看到。)在您的示例中,列表很小,创建完整列表会很快,并且您实际上只是在测量迭代的速度(您不会在其他任何地方花费时间。)尝试使用 python -m timeit -s "s = 'hello world' * 10000" "' '.join(c for c in s) 代替,您会发现生成器可以更快。
  • @Thomas:好点,但对于您的示例,我仍然让生成器变慢(11 毫秒对 8 毫秒),并且进一步增加字符串长度不会改变这一点。
  • 尽管您可能希望将循环更改为c for c in s if 0 以减少创建结果字符串的噪音:)
  • 是的,我忘记了这里涉及的优化,尤其是字符串实习。只需最少的工作量,就不会轻易发现差异;您需要将列表 itself 增加到超出内存的范围,字符串不会占用任何额外的内存。使用字符串以外的东西也可能会显示出更好的差异。
  • 这是一个显示不只是缓存字符串时的差异的版本:paste.pocoo.org/show/273935
【解决方案2】:

两者不等价。

j=i
while j < ls - 1 and len(wordlist[j]) > lc: 
    j+=1

将在 wordlist[j]

j = next(j for j in range(i,ls) if len(wordlist[j]) <=  lc)

将继续遍历整个范围 i 到 ls,而不管列表中单词的长度。

编辑:忽略上述 - 正如 Amber 指出的,对 next() 的调用意味着生成器表达式仅在返回第一个结果之前进行评估。 在那种情况下,我怀疑时差来自使用 range() 而不是 xrange() (除非这是 Python 3.x)。在 Python 2.x 中 range() 将在内存中创建完整列表,即使生成器表达式只返回第一个值。

【讨论】:

  • 实际上并非如此。生成器是惰性求值的,因此调用next() 只会获取结果中的第一个元素,这意味着生成器不会评估任何超出if 条件为真的元素。
  • @Amber:该死的,你是对的。我完全忽略了调用 next()。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多