s=s+c 字符串连接优化是如何决定的?
How is the s=s+c string concat optimization decided?
简写:如果s
是一个字符串,那么s = s + 'c'
可能就地修改字符串,而t = s + 'c'
不能.但是操作 s + 'c'
怎么知道它在哪个场景中呢?
长版:
t = s + 'c'
需要创建一个单独的字符串,因为程序之后需要旧字符串作为 s
和新字符串作为 t
.
如果 s
是唯一的引用,s = s + 'c'
可以就地修改字符串,因为程序只希望 s
成为扩展字符串。如果额外字符的末尾有 space,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
(Try it online!)的基准测试结果:
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
这是来自 Python 3.10 分支的 code in question(在 ceval.c
中,并从同一文件的 BINARY_ADD
操作码实现中调用)。正如@jasonharper 在评论中指出的那样,它会提前查看 BINARY_ADD
的结果接下来是否会绑定到与左侧加数相同的名称。在 fast()
中是(操作数来自 s
,结果存储在 s
中),但在 slow()
中不是(操作数来自 s
但存储到 t
).
但不能保证此优化会持续存在。例如,我注意到你的 fast()
在当前开发 CPython main
分支上并不比你的 slow()
快(这是当前正在进行的工作最终 3.11 版本)。
人们应该依赖这个吗?
如前所述,无法保证此优化会持续存在。 “严肃的”Python 程序员应该知道不要依赖狡猾的 CPython 特有的技巧,事实上,PEP 8 明确警告不要依赖这个特定的技巧:
Code should be written in a way that does not disadvantage other implementations of Python (PyPy, Jython, IronPython, Cython, Psyco, and such).
For example, do not rely on CPython's efficient implementation of in-place string concatenation for statements in the form a += b
or a = a + b
...
简写:如果s
是一个字符串,那么s = s + 'c'
可能就地修改字符串,而t = s + 'c'
不能.但是操作 s + 'c'
怎么知道它在哪个场景中呢?
长版:
t = s + 'c'
需要创建一个单独的字符串,因为程序之后需要旧字符串作为 s
和新字符串作为 t
.
s
是唯一的引用,s = s + 'c'
可以就地修改字符串,因为程序只希望 s
成为扩展字符串。如果额外字符的末尾有 space,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
(Try it online!)的基准测试结果:
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
这是来自 Python 3.10 分支的 code in question(在 ceval.c
中,并从同一文件的 BINARY_ADD
操作码实现中调用)。正如@jasonharper 在评论中指出的那样,它会提前查看 BINARY_ADD
的结果接下来是否会绑定到与左侧加数相同的名称。在 fast()
中是(操作数来自 s
,结果存储在 s
中),但在 slow()
中不是(操作数来自 s
但存储到 t
).
但不能保证此优化会持续存在。例如,我注意到你的 fast()
在当前开发 CPython main
分支上并不比你的 slow()
快(这是当前正在进行的工作最终 3.11 版本)。
人们应该依赖这个吗?
如前所述,无法保证此优化会持续存在。 “严肃的”Python 程序员应该知道不要依赖狡猾的 CPython 特有的技巧,事实上,PEP 8 明确警告不要依赖这个特定的技巧:
Code should be written in a way that does not disadvantage other implementations of Python (PyPy, Jython, IronPython, Cython, Psyco, and such).
For example, do not rely on CPython's efficient implementation of in-place string concatenation for statements in the form
a += b
ora = a + b
...