为什么我的“爆炸”Python代码实际上运行得更快?

时间:2009-09-02 19:40:11

标签: python optimization

我参加了一个入门的comp-sci课程(经过多年的网络编程),并且对我的单行程中获得的速度感到好奇。

for line in lines:
  numbers.append(eval(line.strip().split()[0]))

所以我用痛苦的显式作业写了同样的东西,然后互相对抗。

for line in lines:
  a = line.split()
  b = a[0]
  c = b.strip()
  d = eval(c)
  numbers.append(d)

第二个使用100K行的输入文件运行一致的30ms 更快(在我的FreeBSD shell帐户上;参见编辑#2)!当然这是在3秒的总运行时间,因此百分比并不大...但我真的很惊讶看到所有这些明确命名的任务以某种方式帮助

与内联代码相比,函数的性能有recent thread,但这似乎更基本。是什么赋予了?我是否应该亲切地编写冗长冗长的代码并告诉我的嘲笑同事这是出于性能原因? (值得庆幸的是,列表理解版本运行速度提高了大约10ms,因此我所珍视的紧凑性并不完全在窗外。)

编辑: 感谢您对我的草率扩展代码的提示。你是第二个应该是真的是正确的:

for line in lines:
  a = line.strip()
  b = a.split()
  c = b[0]
  d = eval(c)
  numbers.append(d)

然而,即使我已经解决了这个问题,我的时间分别是2.714s,2.652s和2.624s,对于单线程,完全爆炸形式和列表理解(未图示)。所以我的问题就出现了!

编辑#2: 有趣的是,即使对于一群知识渊博的人来说,答案似乎并不明显,这让我感觉好一点题!在这种情况和类似的情况下,我现在可能会自己玩一下,看看会发生什么。如果你愿意的话,一定要继续修补线程,但我要宣布我收到的答案是“嗯,这很有趣;必须是深刻的东西。”特别是因为行为不是正如史蒂夫指出的那样,机器之间保持一致 - 在我的Debian和Windows安装上,另一个方向略有不同。感谢所有贡献的人!

8 个答案:

答案 0 :(得分:15)

您的代码未按相同顺序展开。紧凑版本:

A > B > C > D > E 

当您的爆炸版本出现

B > C > A > D > E

效果是strip()被推迟2步,这可能会影响性能,具体取决于输入的内容。

答案 1 :(得分:7)

坦率地说,第一个版本,一切都在一行,是一个难以阅读 第二个可能有点过于冗长(中间的东西会被欣赏),但它肯定更好。

由于Python内部原因,我不太关心微优化,只关注可读代码。

顺便说一句:两个(初始)版本没有做同样的事情 在前者中,首先剥离,然后分割,而在后者中首先分割然后剥离(此外,只有第一个元素)。
同样,我认为你忽略了这一点,因为前一版本很难关注 然后,使用dispython disassembler)分析两个(更新的)版本,显示两个代码之间没有真正的区别,只显示了如何查找函数名称的顺序。这可能会对性能产生影响。

虽然我们正在讨论这个问题,但只需在循环之前将eval绑定到局部变量,就可以获得一些性能提升。我希望在这个改变之后,两个版本之间的时间应该没有差别 例如:

eval_ = eval
for line in lines:
    a = line.strip()
    b = a.split()
    c = b[0]
    d = eval_(c)
    numbers.append(d)

我们主要讨论的是微优化,但这种混叠实际上是一种在某些情况下可能非常有用的技术。

答案 2 :(得分:4)

方法调用的顺序也不一样:

for line in lines:
    numbers.append(eval(line.strip().split()[0]))

应该是:

for line in lines:
    numbers.append(eval(line.split()[0].strip()))

答案 3 :(得分:3)

我同意Roberto Liffredo;不要担心这种小的性能提升;更容易理解,调试和更改的代码是它自己的奖励。

至于发生了什么:简洁的代码和扩展的代码并没有做同样的事情。 line.strip().split()首先剥离线然后将其分开;您的扩展代码首先拆分该行,然后在该行的第一个单词上调用strip()。现在,这里不需要strip();它从行尾剥去空格,split()返回的单词从未有过。因此,在您的扩展版本中,strip()完全没有工作要做。

没有基准测试,我无法确定,但我认为strip()没有工作要做是关键。在单行版本中,strip()有时会有工作要做;所以它将剥离空格,构建一个新的字符串对象,然后返回该字符串对象。然后,将拆分并丢弃该新的字符串对象。创建和丢弃字符串对象的额外工作可能是使单行解决方案变慢的原因。将其与扩展版本进行比较,其中strip()仅查看字符串,确定它没有工作要做,并返回未修改的字符串。

总之,我预测与扩展代码等效的单行代码将比扩展代码稍快一些。尝试对此进行基准测试:

for line in lines:
  numbers.append(eval(line.split()[0].strip()))

如果您想要彻底彻底,可以在完全删除strip()的情况下对这两个版本进行基准测试。你根本就不需要它。或者,您可以预处理输入文件,确保任何输入行上没有前导空格或尾随空格,因此strip()也不会有任何工作,您可能会看到基准测试工作为你会期待的。

如果你真的想在这里优化速度,你可以用“maxsplit”参数调用split;您不需要处理整个字符串,因为您在第一次拆分后扔掉了所有东西。因此,您可以致电split(None, 1)。当然,你可以摆脱strip()。然后你会有:

for line in lines:
  numbers.append(eval(line.split(None, 1)[0]))

如果你知道这些数字总是整数,你可以拨打int()而不是eval(),以提高速度和提高安全性。

答案 4 :(得分:3)

此外,有时运行基准测试很棘手。您是否多次重新运行基准并充分利用多次运行?缓存效果是否有可能为您运行的第二个Python程序带来性能优势?您是否尝试过将输入文件放大十倍,那么您的程序运行时间大约需要十倍?

答案 5 :(得分:3)

我没有对它进行基准测试,但时差的一个因素是你必须在第二个函数中做几个变量查找。

From Python Patterns - An Optimization Anecdote

  

这是因为局部变量查找比全局或内置变量查找要快得多:Python“编译器”优化了大多数函数体,因此对于局部变量,不需要字典查找,但是简单的数组索引操作就足够了。

因此,局部变量查找与成本相关联。我们来看看反汇编的函数:

首先,确保我具有与您相同的定义功能:

>>> def a(lines):
    for line in lines:
        numbers.append(eval(line.strip().split()[0]))

>>> def b(lines):
    for line in lines:
        a = line.strip()
        b = a.split()
        c = b[0]
        d = eval(c)
        numbers.append(d)

现在,让我们比较他们的反汇编值:

>>> import dis
>>> dis.dis(a)
  2           0 SETUP_LOOP              49 (to 52)
              3 LOAD_FAST                0 (lines)
              6 GET_ITER            
        >>    7 FOR_ITER                41 (to 51)
             10 STORE_FAST               1 (line)

  3          13 LOAD_GLOBAL              0 (numbers)
             16 LOAD_ATTR                1 (append)
             19 LOAD_GLOBAL              2 (eval)
             22 LOAD_FAST                1 (line)
             25 LOAD_ATTR                3 (strip)
             28 CALL_FUNCTION            0
             31 LOAD_ATTR                4 (split)
             34 CALL_FUNCTION            0
             37 LOAD_CONST               1 (0)
             40 BINARY_SUBSCR       
             41 CALL_FUNCTION            1
             44 CALL_FUNCTION            1
             47 POP_TOP             
             48 JUMP_ABSOLUTE            7
        >>   51 POP_BLOCK           
        >>   52 LOAD_CONST               0 (None)
             55 RETURN_VALUE        
>>> dis.dis(b)
  2           0 SETUP_LOOP              73 (to 76)
              3 LOAD_FAST                0 (lines)
              6 GET_ITER            
        >>    7 FOR_ITER                65 (to 75)
             10 STORE_FAST               1 (line)

  3          13 LOAD_FAST                1 (line)
             16 LOAD_ATTR                0 (strip)
             19 CALL_FUNCTION            0
             22 STORE_FAST               2 (a)

  4          25 LOAD_FAST                2 (a)
             28 LOAD_ATTR                1 (split)
             31 CALL_FUNCTION            0
             34 STORE_FAST               3 (b)

  5          37 LOAD_FAST                3 (b)
             40 LOAD_CONST               1 (0)
             43 BINARY_SUBSCR       
             44 STORE_FAST               4 (c)

  6          47 LOAD_GLOBAL              2 (eval)
             50 LOAD_FAST                4 (c)
             53 CALL_FUNCTION            1
             56 STORE_FAST               5 (d)

  7          59 LOAD_GLOBAL              3 (numbers)
             62 LOAD_ATTR                4 (append)
             65 LOAD_FAST                5 (d)
             68 CALL_FUNCTION            1
             71 POP_TOP             
             72 JUMP_ABSOLUTE            7
        >>   75 POP_BLOCK           
        >>   76 LOAD_CONST               0 (None)
             79 RETURN_VALUE        

这是很多信息,但我们可以看到由于使用了局部变量,第二种方法充满了STORE_FASTLOAD_FAST对。除了其他人提到的不同的操作顺序之外,可能足以导致您的小时间差异(可能)。

答案 6 :(得分:2)

单线并不意味着更小或更快的代码。而且我希望eval()行能够摒弃性能测量。

如果没有eval,你会看到类似的性能差异吗?

答案 7 :(得分:2)

好的,足够的理论化。我创建了一个包含一百万行的文件,在每行的开头和结尾都有随机数量的空格(0到4个空格,通常为0)。我运行你的单行程序,扩展版本和我自己的列表理解版本(尽我所知如何制作它)。

我的结果? (每个是三个试验中最好的一个):

one-line: 13.208s
expanded: 26.321s
listcomp: 13.024s

我在Ubuntu 9.04,32位下测试,使用Python 2.6.2(当然是CPython)。

所以我完全无法解释为什么你看到扩展的那个运行得更快,因为它在我的计算机上运行 half

这是我用来生成测试数据的Python程序:

import random

f = open("/tmp/junk.txt", "w")

r = random.Random()

def randws():
    n = r.randint(0, 10) - 4
    if n < 0 or n > 4:
        n = 0
    return " " * n

for i in xrange(1000000):
    s0 = randws()
    n = r.randint(0, 256)
    s1 = randws()
    f.write("%s%d%s\n" % (s0, n, s1))

这是我的列表理解程序:

lines = open("/tmp/junk.txt")

numbers = [eval(line.split(None, 1)[0]) for line in lines]

P.S。这是一个很好的快速版本,可以处理intfloat值。

lines = open("/tmp/junk.txt")

def val(x):
    try:
        return int(x)
    except ValueError:
        pass

    try:
        return float(x)
    except StandardError:
        return 0

numbers = [val(line.split(None, 1)[0]) for line in lines]

最好的三个时间是:2.161s