【问题标题】:Why is variable1 += variable2 much faster than variable1 = variable1 + variable2?为什么 variable1 += variable2 比 variable1 = variable1 + variable2 快得多?
【发布时间】:2014-10-19 15:41:33
【问题描述】:

我继承了一些 Python 代码,这些代码用于创建巨大的表(最多 19 列宽,5000 行)。在屏幕上绘制表格花了 9 秒。我注意到每一行都是使用以下代码添加的:

sTable = sTable + '\n' + GetRow()

sTable 是一个字符串。

我把它改成了:

sTable += '\n' + GetRow()

我注意到表格现在出现在 六秒

然后我把它改成:

sTable += '\n%s' % GetRow()

基于these Python performance tips(仍然是六秒)。

由于这被调用了大约 5000 次,它突出了性能问题。但是为什么会有这么大的差异呢?为什么编译器没有在第一个版本中发现问题并对其进行优化?

【问题讨论】:

  • 由于没有静态类型,几乎没有机会使用 C 等语言进行编译时优化,而 CPython 没有 JIT。此外,+=+= 的效果不同,因此需要进行一些额外的分析才能确定还存在哪些对sTable 的引用。
  • 连接字符串很慢。您可能希望使用canonical way 并将您的行收集到一个列表中,并在获得所有行后join 他们:table = ''.join(rows)
  • 在 100k concats 上计时,+=.06ms 使用 python 2.7.8 更快
  • 谢谢@Matthias - 我也试过了(另请参阅链接的问题),但令人惊讶的是,这没有性能差异。

标签: python html string performance python-internals


【解决方案1】:

这不是关于使用就地 +=+ 二进制添加。你没有告诉我们整个故事。您的原始版本连接了 3 个字符串,而不仅仅是两个:

sTable = sTable + '\n' + sRow  # simplified, sRow is a function call

Python 试图帮助和优化字符串连接;在使用strobj += otherstrobjstrobj = strobj + otherstringobj 时都适用,但当涉及超过2 个字符串时,它无法应用此优化。

Python 字符串通常是不可变的,但是如果没有其他对左侧字符串对象的引用并且它无论如何都会被反弹,那么 Python 作弊和 改变字符串。这样可以避免每次连接时都必须创建一个新字符串,从而大大提高速度。

这是在字节码评估循环中实现的。在使用BINARY_ADD on two stringsINPLACE_ADD on two strings 时,Python 将连接委托给一个特殊的辅助函数string_concatenate()。为了能够通过改变字符串来优化连接,首先需要确保字符串没有其他对它的引用;如果只有堆栈和原始变量引用它,则可以这样做,并且下一个操作将替换原始变量引用。

因此,如果只有 2 个对字符串的引用,并且下一个运算符是 STORE_FAST(设置局部变量)、STORE_DEREF(设置封闭函数引用的变量)或 STORE_NAME(设置一个全局变量),并且受影响的变量当前引用了相同的字符串,则清除该目标变量以将引用数量减少到只有 1 个,即堆栈。

这就是为什么您的原始代码无法完全使用此优化的原因。您的表达式的第一部分是sTable + '\n'next 操作是另一个BINARY_ADD

>>> import dis
>>> dis.dis(compile(r"sTable = sTable + '\n' + sRow", '<stdin>', 'exec'))
  1           0 LOAD_NAME                0 (sTable)
              3 LOAD_CONST               0 ('\n')
              6 BINARY_ADD          
              7 LOAD_NAME                1 (sRow)
             10 BINARY_ADD          
             11 STORE_NAME               0 (sTable)
             14 LOAD_CONST               1 (None)
             17 RETURN_VALUE        

第一个BINARY_ADD 后跟LOAD_NAME 以访问sRow 变量,而不是存储操作。第一个BINARY_ADD 必须始终产生一个新的字符串对象,随着sTable 的增长而变得越来越大,并且创建这个新的字符串对象需要越来越多的时间。

您将此代码更改为:

sTable += '\n%s' % sRow

其中删除了第二个串联。现在字节码是:

>>> dis.dis(compile(r"sTable += '\n%s' % sRow", '<stdin>', 'exec'))
  1           0 LOAD_NAME                0 (sTable)
              3 LOAD_CONST               0 ('\n%s')
              6 LOAD_NAME                1 (sRow)
              9 BINARY_MODULO       
             10 INPLACE_ADD         
             11 STORE_NAME               0 (sTable)
             14 LOAD_CONST               1 (None)
             17 RETURN_VALUE        

我们只剩下一个INPLACE_ADD 和一个商店。现在sTable 可以就地更改,不会产生更大的新字符串对象。

你会得到相同的速度差异:

sTable = sTable + ('\n%s' % sRow)

这里。

计时赛显示了差异:

>>> import random
>>> from timeit import timeit
>>> testlist = [''.join([chr(random.randint(48, 127)) for _ in range(random.randrange(10, 30))]) for _ in range(1000)]
>>> def str_threevalue_concat(lst):
...     res = ''
...     for elem in lst:
...         res = res + '\n' + elem
... 
>>> def str_twovalue_concat(lst):
...     res = ''
...     for elem in lst:
...         res = res + ('\n%s' % elem)
... 
>>> timeit('f(l)', 'from __main__ import testlist as l, str_threevalue_concat as f', number=10000)
6.196403980255127
>>> timeit('f(l)', 'from __main__ import testlist as l, str_twovalue_concat as f', number=10000)
2.3599119186401367

这个故事的寓意是你不应该首先使用字符串连接。从大量其他字符串构建新字符串的正确方法是使用列表,然后使用str.join()

table_rows = []
for something in something_else:
    table_rows += ['\n', GetRow()]
sTable = ''.join(table_rows)

这还是更快:

>>> def str_join_concat(lst):
...     res = ''.join(['\n%s' % elem for elem in lst])
... 
>>> timeit('f(l)', 'from __main__ import testlist as l, str_join_concat as f', number=10000)
1.7978830337524414

但你不能仅仅使用'\n'.join(lst):

>>> timeit('f(l)', 'from __main__ import testlist as l, nl_join_concat as f', number=10000)
0.23735499382019043

【讨论】:

  • 嗯。所以是字符串拼接优化造成了差异,而代码最初只是以无法触发优化的方式编写的。
猜你喜欢
  • 1970-01-01
  • 2019-06-21
  • 2019-03-05
  • 2017-08-05
  • 1970-01-01
  • 2014-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多