__add__和+运算符之间的性能差异

时间:2019-07-12 09:33:05

标签: python performance

我正在阅读学习Python第5版,并且我需要对此段进行更多说明:

  

例如,字符串的__add__方法真正执行连接;尽管内部通常不应该自己使用第二种形式,但Python内部将以下第一种形式映射到第二种形式(它不那么直观,甚至可能运行得更慢):

>>> S+'NI!'
'spamNI!'
>>> S.__add__('NI!')
'spamNI!'

所以我的问题是,为什么运行速度会变慢?

2 个答案:

答案 0 :(得分:7)

>>> def test(a, b):
...     return a + b
... 
>>> def test2(a, b):
...     return a.__add__(b)
... 
>>> import dis
>>> dis.dis(test)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD          
              7 RETURN_VALUE        
>>> dis.dis(test2)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_ATTR                0 (__add__)
              6 LOAD_FAST                1 (b)
              9 CALL_FUNCTION            1
             12 RETURN_VALUE        

1条BINARY_ADD指令,而不是2条指令:LOAD_ATTRCALL_FUNCTION。而且由于BINARY_ADD(几乎)做同样的事情(但在C语言中),所以我们可以期望它(略)更快。不过,这种差异几乎不会引起注意。

旁注:,所以这与汇编的工作方式相似。通常,只有一条指令执行与一系列指令相同的操作时,它会更好地执行。例如,在x64 LEA中,指令可以替换为其他指令序列。但是他们的表现不佳。

但是有一个陷阱(这说明了为什么我开始谈论x64汇编的原因)。有时,一条指令实际上会表现更差。参见臭名昭著的LOOP instruction。这种违反直觉的行为可能有很多原因,例如:有点不同的假设,未优化的实现,历史原因,错误等,等等。

结论:在Python +中,理论上应该比__add__快,但始终可以测量

答案 1 :(得分:4)

可能是解释说+运算符实际上会在后台调用__add__。因此,当您执行S + 'NI!'时,实际上会__add__被调用( if S有一个)。因此,从语义上讲,两个版本的功能完全相同。

尽管区别在于代码对应的内容。您可能知道,Python被编译成字节码,然后执行。字节码操作决定了解释器必须执行的步骤。您可以使用dis模块查看字节码:

>>> import dis
>>> dis.dis("S+'NI!'")
  1           0 LOAD_NAME                0 (S)
              2 LOAD_CONST               0 ('NI!')
              4 BINARY_ADD
              6 RETURN_VALUE
>>> dis.dis("S.__add__('NI!')")
  1           0 LOAD_NAME                0 (S)
              2 LOAD_METHOD              1 (__add__)
              4 LOAD_CONST               0 ('NI!')
              6 CALL_METHOD              1

如您所见,这里的区别基本上是+运算符只是执行BINARY_ADD__add__调用加载实际方法并执行它。

当解释器看到BINARY_ADD时,它将自动查找__add__实现并调用该实现,但与必须在Python字节码中查找该方法相比,它可以更有效地实现该功能。 / p>

因此,基本上,通过显式调用__add__,可以防止解释器更快地转到实现。

话虽这么说,差异可忽略不计。如果您计时两个呼叫之间的时间差,您可以看到,但实际上并没有那么多(这是1000万次呼叫):

>>> timeit("S+'NI!'", setup='S = "spam"', number=10**7)
0.45791053899995404
>>> timeit("S.__add__('NI!')", setup='S = "spam"', number=10**7)
1.0082074819999889

请注意,这些结果不一定总是看起来像这样。计时自定义类型(使用非常简单的__add__实现)时,对__add__的调用可能会更快:

>>> timeit("S+'NI!'", setup='from __main__ import SType;S = SType()', number=10**7)
0.7971681049998551
>>> timeit("S.__add__('NI!')", setup='from __main__ import SType;S = SType()', number=10**7)
0.6606798959999196

这里的差异更小,但+ 慢。

最重要的是,您不必担心这些差异。选择更具可读性的内容,几乎所有时间都是+。如果您需要担心性能,请确保从整体上分析您的应用程序,并且不要相信这种微基准。当您查看您的应用程序时,它们没有帮助,并且99.99%的情况下,这两种方法之间的差异不会有所不同。您的应用程序中还有另一个瓶颈会使其运行速度进一步降低。