简短版:如果s
是一个字符串,那么s = s + 'c'
可能会就地修改字符串,而t = s + 'c'
则不行。但是,s + 'c'
操作如何知道它处于哪种情况?
详细版:
t = s + 'c'
需要创建一个单独的字符串,因为程序之后想要旧字符串作为s
,新字符串作为t
。
s = s + 'c'
可以在原地修改字符串,如果s
是唯一的引用,因为程序只想让s
成为扩展后的字符串。CPython实际上会进行这种优化,如果末尾有额外的空间。
考虑这些函数,它们重复添加一个字符:
def fast(n):
s = ''
for _ in range(n):
s = s + 'c'
t = s
del t
def slow(n):
s = ''
for _ in range(n):
t = s + 'c'
s = t
del t
使用n = 100_000
的基准测试结果(在线尝试!):
fast : 9 ms 9 ms 9 ms 9 ms 10 ms
slow : 924 ms 927 ms 931 ms 933 ms 945 ms
请注意,额外的
t = s
或 s = t
使得两个变量都等价地引用字符串,接着 del t
只留下了 s
,因此在下一次循环迭代中,s
再次成为唯一引用该字符串的变量。因此,这两个函数之间唯一的区别是将 s + 'c'
赋给 s
和 t
的顺序。我们还要反汇编字节码。我用
!=
标记了唯一的三个不同点。如预期所述,只有 STORE_FAST
和 LOAD_FAST
的变量不同。但是,在包括 BINARY_ADD
在内的指令序列直到最后一个都是相同的。那么,BINARY_ADD
如何知道是否进行了优化呢? import dis import dis
dis.dis(fast) dis.dis(slow)
---------------------------------------------------------------------------
0 LOAD_CONST 1 ('') 0 LOAD_CONST 1 ('')
2 STORE_FAST 1 (s) 2 STORE_FAST 1 (s)
4 LOAD_GLOBAL 0 (range) 4 LOAD_GLOBAL 0 (range)
6 LOAD_FAST 0 (n) 6 LOAD_FAST 0 (n)
8 CALL_FUNCTION 1 8 CALL_FUNCTION 1
10 GET_ITER 10 GET_ITER
>> 12 FOR_ITER 18 (to 32) >> 12 FOR_ITER 18 (to 32)
14 STORE_FAST 2 (_) 14 STORE_FAST 2 (_)
16 LOAD_FAST 1 (s) 16 LOAD_FAST 1 (s)
18 LOAD_CONST 2 ('c') 18 LOAD_CONST 2 ('c')
20 BINARY_ADD 20 BINARY_ADD
22 STORE_FAST 1 (s) != 22 STORE_FAST 3 (t)
24 LOAD_FAST 1 (s) != 24 LOAD_FAST 3 (t)
26 STORE_FAST 3 (t) != 26 STORE_FAST 1 (s)
28 DELETE_FAST 3 (t) 28 DELETE_FAST 3 (t)
30 JUMP_ABSOLUTE 12 30 JUMP_ABSOLUTE 12
>> 32 LOAD_CONST 0 (None) >> 32 LOAD_CONST 0 (None)
34 RETURN_VALUE 34 RETURN_VALUE
string_concatenate()
函数中(在Python/ceval.c中),该函数被调用以将两个字符串进行BINARY_ADD操作。它会预先查看下一个字节码指令,并且只有在该指令是对加法的第一个操作数进行STORE操作时,才会考虑进行原地修改。 - jasonharper