为什么我的“爆炸” Python 代码反而运行得更快?

3 投票
8 回答
953 浏览
提问于 2025-04-15 14:02

我在一门入门的计算机科学课上(之前已经做了几年网页编程),对我的一行代码到底能提高多少速度产生了好奇。

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)

结果第二种写法的运行速度稳定快了30毫秒(在我的FreeBSD shell账户上;见编辑#2),输入文件有10万行!当然,这个总运行时间是3秒,所以这个百分比并不大……但我真的很惊讶,看到那些明确的变量赋值居然有帮助。

最近有一个关于函数性能和内联代码性能的讨论,但这看起来更基础。到底是怎么回事?我是不是应该写得详细一点,告诉那些嘲笑我的同事这是为了性能考虑?(幸运的是,列表推导的版本运行得更快,大约快10毫秒,所以我心爱的简洁性并没有完全失去。)

编辑:感谢大家指出我代码扩展得不够好的问题。你们都说得对,第二种写法应该是:

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

不过,即使我修正了这个问题,我的运行时间分别是2.714秒、2.652秒和2.624秒,分别对应一行代码、完全展开的形式和列表推导(未显示)。所以我的问题依然存在!

编辑#2:有趣的是,似乎连一群知识渊博的人都觉得这个答案不明显,这让我对这个问题感到好一些!我可能会在类似情况下自己再研究一下,看看会有什么发现。如果你们想继续讨论这个话题,当然可以,但我决定把我的结论定为“嗯,这很有趣;一定有什么深层次的原因。”特别是因为正如steveha指出的那样,这种表现并不是在所有机器上都一致,我在Debian和Windows上的结果正好相反。感谢所有参与讨论的人!

8 个回答

7

老实说,第一种版本把所有东西都放在一行里,真的是看得让人头疼。
第二种版本可能有点啰嗦(中间的那种会更好),但绝对比第一种好。

我觉得没必要太在意微小的优化,因为Python的内部机制,还是应该专注于写出可读性强的代码。

顺便提一下,这两个(最初的)版本其实做的事情不一样。
在第一个版本里,你是先去掉空格,然后再分割;而在第二个版本里,你是先分割,再去掉空格(而且只对第一个元素去掉空格)。
我觉得你可能忽略了这一点,因为第一个版本真的很难集中注意力去看。

接着,分析这两个(更新后的)版本时用到了disPython反汇编工具),结果显示这两段代码之间没有实质性的差别,唯一不同的就是函数名称查找的顺序。这可能会对性能有影响。

既然我们提到这个,你可以通过在循环之前把eval绑定到一个局部变量上来获得一些性能提升。我预计在做了这个改动后,这两个版本的执行时间应该没有区别。
比如:

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

我们主要讨论的是微小的优化,但这种别名技术在某些情况下其实是非常有用的。

15

你的代码执行的顺序不一样。紧凑版的执行顺序是:

A > B > C > D > E 

而你展开的版本执行顺序是:

B > C > A > D > E

这样一来,strip()这个操作被推迟了两步,这可能会影响性能,具体取决于输入是什么。

3

我没有进行过性能测试,但时间差异的一个原因是第二个函数需要进行多次变量查找。

来自Python Patterns - 一个优化的故事

这是因为查找局部变量的速度比查找全局变量或内置变量要快得多: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的组合,这是因为使用了局部变量。这可能是导致你观察到的小时间差的原因之一,此外还有其他人提到的不同操作顺序。

撰写回答