Python 为什么变量为1+;=variable2比variable1快得多=variable1+;变量2?

Python 为什么变量为1+;=variable2比variable1快得多=variable1+;变量2?,python,html,string,performance,python-internals,Python,Html,String,Performance,Python Internals,我继承了一些用于创建大型表(最多19列宽5000行)的Python代码。在屏幕上绘制表格花了九秒钟。我注意到每一行都是使用以下代码添加的: sTable = sTable + '\n' + GetRow() 其中,sTable是一个字符串 我改为: sTable += '\n' + GetRow() 我注意到桌子现在在六秒钟内出现了 然后我把它改成: sTable += '\n%s' % GetRow() 基于(仍然是6秒) 由于调用了大约5000次,它突出了性能问题。但为什么会有如此大的

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

sTable = sTable + '\n' + GetRow()
其中,
sTable
是一个字符串

我改为:

sTable += '\n' + GetRow()
我注意到桌子现在在六秒钟内出现了

然后我把它改成:

sTable += '\n%s' % GetRow()
基于(仍然是6秒)


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

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

sTable = sTable + '\n' + sRow  # simplified, sRow is a function call
Python试图帮助解决并优化字符串连接;当使用
strobj+=otherstrobj
strobj=strobj+otherstringobj
时,但当涉及两个以上的字符串时,它不能应用此优化

Python字符串通常是不可变的,但是如果没有对左手字符串对象的其他引用,并且该对象仍在反弹,那么Python会欺骗并变异该字符串。这避免了每次连接时都必须创建一个新字符串,这可以大大提高速度

这是在字节码评估循环中实现的。无论是在使用还是在使用时,Python都会将连接委托给一个特殊的助手函数。为了能够通过改变字符串来优化连接,首先需要确保字符串没有其他引用;如果只有堆栈和原始变量引用它,则可以执行此操作,下一个
操作将替换原始变量引用

因此,如果只有2个字符串引用,下一个操作符是
STORE\u FAST
(设置局部变量)、
STORE\u DEREF
(设置封闭函数引用的变量)或
STORE\u NAME
(设置全局变量)中的一个,并且受影响的变量当前引用相同的字符串,然后清除该目标变量,将引用数减少到堆栈中的1

这就是为什么您的原始代码不能完全使用这种优化。表达式的第一部分是
sTable+'\n'
,下一个操作是另一个
BINARY\u ADD

它删除了第二个连接。现在字节码是:

>>> 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        
在这里

计时试验显示了不同之处:

>>> 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()

这个速度更快:

>>> 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”来击败。加入(lst)


由于没有静态类型,因此很少有机会从C等语言进行编译时优化,而且CPython没有JIT。另外,
+=
+
=
有不同的效果,因此需要进行一些额外的分析,以确定是否存在对
稳定的
的其他引用。串接字符串的速度很慢。您可能希望使用并在列表中收集您的行,并在获得所有行之后收集它们:
table='''.join(rows)
。在100k concats上计时,
+=
使用python 2.7.8要快些
.06ms
,谢谢@Matthias-我也尝试过了(另请参见链接的问题),但没有性能差异,令人惊讶。@Wikis:看到了吧。因此,是字符串串联优化导致了差异,而代码最初的编写方式是优化无法触发的。
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
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
>>> timeit('f(l)', 'from __main__ import testlist as l, nl_join_concat as f', number=10000)
0.23735499382019043