为什么变量 1 += 变量 2 比变量 1 = 变量 1 + 变量 2 快得多?

问题描述 投票:0回答:1

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

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

其中

sTable
是一个字符串。

我将其更改为:

sTable += '\n' + GetRow()

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

然后我将其更改为:

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

基于 这些 Python 性能技巧(还有六秒)。

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

python html string performance python-internals
1个回答
91
投票

这不是关于使用就地

+=
+
二进制加法。你没有告诉我们整个故事。你的原始版本连接了 3 个字符串,而不仅仅是两个:

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

Python 尝试帮助并优化字符串连接;使用

strobj += otherstrobj
strobj = strobj + otherstringobj
时都可以,但是当涉及超过 2 个字符串时,无法应用此优化。

Python 字符串通常是不可变的,但是如果没有其他对左侧字符串对象的引用并且它无论如何都会被反弹,那么 Python 就会作弊并改变字符串。这避免了每次连接时都必须创建新字符串,这可以大大提高速度。 这是在字节码评估循环中实现的。在两个字符串

上使用

BINARY_ADD

 和在两个字符串
上使用 INPLACE_ADD
 时,Python 都会将连接委托给特殊的辅助函数 
string_concatenate()
。为了能够通过改变字符串来优化串联,首先需要确保该字符串没有其他引用;如果只有堆栈和原始变量引用它,那么这可以完成,
and
next操作将替换原始变量引用。 因此,如果只有 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

	
© www.soinside.com 2019 - 2024. All rights reserved.