Why is variable1 += variable2 much faster than variable1 = variable1 + variable2?
我继承了一些用于创建大型表(宽达19列、宽达5000行)的python代码。在屏幕上画桌子花了九秒钟。我注意到每行都是使用以下代码添加的:
1 2 | sTable = sTable + ' ' + GetRow() |
其中,
我把它改为:
1 2 | sTable += ' ' + GetRow() |
号
我注意到桌子现在六秒钟后就出现了。
然后我把它改成:
1 2 | sTable += ' %s' % GetRow() |
基于这些python性能提示(还有6秒)。
由于调用了大约5000次,它突出了性能问题。但为什么会有如此大的差异呢?为什么编译器没有在第一个版本中发现问题并优化它呢?
这不是使用就地
1 2 | sTable = sTable + ' ' + sRow # simplified, sRow is a function call |
python试图帮助并优化字符串连接;在使用
通常情况下,python字符串是不可变的,但是如果没有对左侧字符串对象的其他引用,并且仍然在反弹,那么python就会欺骗并改变字符串。这样就避免了每次连接时都必须创建一个新字符串,从而可以大大提高速度。
这在字节码评估循环中实现。在两个字符串上使用
因此,如果只有2个对字符串的引用,下一个运算符是
这就是为什么原始代码不能完全使用这种优化的原因。表达式的第一部分是
'
1 2 3 4 5 6 7 8 9 10 11 12 | >>> import dis >>> dis.dis(compile(r"sTable = sTable + ' ' + sRow", '<stdin>', 'exec')) 1 0 LOAD_NAME 0 (sTable) 3 LOAD_CONST 0 (' ') 6 BINARY_ADD 7 LOAD_NAME 1 (sRow) 10 BINARY_ADD 11 STORE_NAME 0 (sTable) 14 LOAD_CONST 1 (None) 17 RETURN_VALUE |
号
第一个
您将此代码更改为:
1 2 | sTable += ' %s' % sRow |
删除了第二个连接。现在字节码是:
1 2 3 4 5 6 7 8 9 10 11 | >>> dis.dis(compile(r"sTable += ' %s' % sRow", '<stdin>', 'exec')) 1 0 LOAD_NAME 0 (sTable) 3 LOAD_CONST 0 (' %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 |
。
我们只剩下一个
你会得到相同的速度差:
1 2 | sTable = sTable + (' %s' % sRow) |
在这里。
时间试验显示不同之处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | >>> 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 + ' ' + elem ... >>> def str_twovalue_concat(lst): ... res = '' ... for elem in lst: ... res = res + (' %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 |
。
这个故事的寓意是,首先不应该使用字符串串联。从其他字符串的负载构建新字符串的正确方法是使用列表,然后使用
1 2 3 4 5 | table_rows = [] for something in something_else: table_rows += [' ', GetRow()] sTable = ''.join(table_rows) |
。
这还是更快的:
1 2 3 4 5 6 | >>> def str_join_concat(lst): ... res = ''.join([' %s' % elem for elem in lst]) ... >>> timeit('f(l)', 'from __main__ import testlist as l, str_join_concat as f', number=10000) 1.7978830337524414 |
但你不能只使用
'.join(lst)
1 2 | >>> timeit('f(l)', 'from __main__ import testlist as l, nl_join_concat as f', number=10000) 0.23735499382019043 |
。