don*_*ode 18 python performance cpython internals
简短版本:如果s是字符串,则s = s + \'c\'可以就地修改字符串,但t = s + \'c\'不能。但是操作如何s + \'c\'知道它处于哪种场景呢?
长版:
\nt = s + \'c\'需要创建一个单独的字符串,因为程序随后需要旧字符串 ass和新字符串 as t。
s = s + \'c\'如果是唯一的引用,则可以就地修改字符串s,因为程序只想s成为扩展字符串。如果末尾有多余字符的空间,CPython 实际上会执行此优化。
考虑这些重复添加字符的函数:
\ndef fast(n):\n s = \'\'\n for _ in range(n):\n s = s + \'c\'\n t = s\n del t\n\ndef slow(n):\n s = \'\'\n for _ in range(n):\n t = s + \'c\'\n s = t\n del t\nRun Code Online (Sandbox Code Playgroud)\n基准测试结果n = 100_000(在线尝试!):
fast : 9 ms 9 ms 9 ms 9 ms 10 ms \nslow : 924 ms 927 ms 931 ms 933 ms 945 ms\nRun Code Online (Sandbox Code Playgroud)\n请注意, extrat = s或s = t使两个变量等效于对字符串的引用,然后del t仅留下s,因此在下一次循环迭代时,s再次成为对字符串的唯一引用。s + \'c\'因此,两个函数之间的唯一区别是分配给s和 的顺序t。
我们还可以反汇编字节码。!=我在中间标记了仅有的三个差异。STORE_FAST正如预期的那样,只有和 的变量LOAD_FAST不同。但在 之前(包括 )BINARY_ADD,字节码是相同的。那么如何知道BINARY_ADD是否要优化呢?
import dis import dis\n dis.dis(fast) dis.dis(slow)\n---------------------------------------------------------------------------\n 0 LOAD_CONST 1 (\'\') 0 LOAD_CONST 1 (\'\')\n 2 STORE_FAST 1 (s) 2 STORE_FAST 1 (s)\n \n 4 LOAD_GLOBAL 0 (range) 4 LOAD_GLOBAL 0 (range)\n 6 LOAD_FAST 0 (n) 6 LOAD_FAST 0 (n)\n 8 CALL_FUNCTION 1 8 CALL_FUNCTION 1\n 10 GET_ITER 10 GET_ITER \n>> 12 FOR_ITER 18 (to 32) >> 12 FOR_ITER 18 (to 32)\n 14 STORE_FAST 2 (_) 14 STORE_FAST 2 (_)\n \n 16 LOAD_FAST 1 (s) 16 LOAD_FAST 1 (s)\n 18 LOAD_CONST 2 (\'c\') 18 LOAD_CONST 2 (\'c\')\n 20 BINARY_ADD 20 BINARY_ADD \n 22 STORE_FAST 1 (s) != 22 STORE_FAST 3 (t)\n \n 24 LOAD_FAST 1 (s) != 24 LOAD_FAST 3 (t)\n 26 STORE_FAST 3 (t) != 26 STORE_FAST 1 (s)\n \n 28 DELETE_FAST 3 (t) 28 DELETE_FAST 3 (t)\n 30 JUMP_ABSOLUTE 12 30 JUMP_ABSOLUTE 12\n>> 32 LOAD_CONST 0 (None) >> 32 LOAD_CONST 0 (None)\n 34 RETURN_VALUE 34 RETURN_VALUE \nRun Code Online (Sandbox Code Playgroud)\n
Tim*_*ers 16
下面是来自 Python 3.10 分支的相关代码ceval.c(在 中,并从同一文件的操作BINARY_ADD码实现中调用)。正如 @jasonharper 在评论中指出的那样,它会向前查看,看看接下来的结果是否BINARY_ADD绑定到左侧加数来自的相同名称。在 中fast(),它是(操作数来自s且结果存储到 中s),但在slow()它中不是(操作数来自s但存储到 中t)。
但不能保证这种优化会持续下去。例如,我注意到您的fast()速度并不比slow()当前开发的 CPythonmain分支(这是当前正在进行的最终 3.11 版本的工作)快。
如前所述,无法保证这种优化会持续存在。“严肃的”Python 程序员应该知道不要依赖狡猾的 CPython 特定技巧,事实上,PEP 8明确警告不要依赖这一特定技巧:
代码的编写方式不应损害 Python 的其他实现(PyPy、Jython、IronPython、Cython、Psyco 等)。
例如,不要依赖 CPython 对以下形式的语句的就地字符串连接的高效实现
a += b或a = a + b...