为啥在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复)
Posted
技术标签:
【中文标题】为啥在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复)【英文标题】:Why were literal formatted strings (f-strings) so slow in Python 3.6 alpha? (now fixed in 3.6 stable)为什么在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复) 【发布时间】:2016-09-18 19:38:14 【问题描述】:我从 Python Github 存储库下载了 Python 3.6 alpha 版本,我最喜欢的新功能之一是文字字符串格式化。可以这样使用:
>>> x = 2
>>> f"x is x"
"x is 2"
这似乎与在str
实例上使用format
函数的作用相同。但是,我注意到的一件事是,与仅调用 format
相比,这种文字字符串格式化实际上非常慢。以下是timeit
对每种方法的说明:
>>> x = 2
>>> timeit.timeit(lambda: f"X is x")
0.8658502227130764
>>> timeit.timeit(lambda: "X is ".format(x))
0.5500578542015617
如果我使用字符串作为timeit
的参数,我的结果仍然显示模式:
>>> timeit.timeit('x = 2; f"X is x"')
0.5786435347381484
>>> timeit.timeit('x = 2; "X is ".format(x)')
0.4145195760771685
如您所见,使用format
几乎花费了一半的时间。我希望文字方法更快,因为涉及的语法更少。幕后发生了什么导致文字方法慢得多?
【问题讨论】:
f 字符串是动态的,因此必须在每个循环中生成字符串;而格式字符串是在代码运行之前创建的文字,当它被转换为字节码时。 @AlexHall 也许这与x
在传递给format
方法时分配给局部变量的事实有关,但必须由@ 在globals
中找到987654333@ 语法。
@AlexHall:这不是错误。底层只是一个不同的实现,因为格式字符串必须在编译时解析,而 str.format()
在 runtime 解析槽。
@PM2Ring:所有表达式在编译时编译并在运行时计算。
@MartijnPieters 如果字符串是在运行时编译的,那应该意味着更少的计算。至少如果.format
更快,那么这些字符串应该简单地编译成对.format
的调用。
【参考方案1】:
在 3.6 beta 1 之前,格式字符串 f'x is x'
被编译为等效于 ''.join(['x is ', x.__format__('')])
。生成的字节码效率低下有几个原因:
-
它构建了一系列字符串片段...
... 这个序列是一个列表,而不是一个元组! (构造元组比构造列表要快一些)。
它将一个空字符串压入堆栈
它在空字符串上查找
join
方法
它甚至在裸 Unicode 对象上调用 __format__
,__format__('')
将始终返回 self
,或整数对象,__format__('')
作为参数返回 str(self)
。
__format__
方法未插入。
但是,对于更复杂和更长的字符串,文字格式的字符串仍然会比相应的 '...'.format(...)
调用更快,因为对于后者,每次格式化字符串时都会解释字符串。
这个问题是issue 27078 要求新的 Python 字节码操作码以将字符串片段转换为字符串的主要动机(操作码获取一个操作数 - 堆栈上的片段数;片段被推送到堆栈中出现的顺序,即最后一部分是最上面的项目)。 Serhiy Storchaka 实现了这个新的操作码并将其合并到 CPython 中,这样它就可以在 Python 3.6 中使用,从 beta 1 版本开始(因此在 Python 3.6.0 最终版本中)。
因此,文字格式的字符串将比string.format
快很多。如果您只是插入 str
或 int
对象,它们通常也比 Python 3.6 中的旧式格式快:
>>> timeit.timeit("x = 2; 'X is '.format(x)")
0.32464265200542286
>>> timeit.timeit("x = 2; 'X is %s' % x")
0.2260766440012958
>>> timeit.timeit("x = 2; f'X is x'")
0.14437875000294298
f'X is x'
现在编译为
>>> dis.dis("f'X is x'")
1 0 LOAD_CONST 0 ('X is ')
2 LOAD_NAME 0 (x)
4 FORMAT_VALUE 0
6 BUILD_STRING 2
8 RETURN_VALUE
新的BUILD_STRING
以及FORMAT_VALUE
代码中的优化完全消除了6 个低效率源中的前5 个。 __format__
方法仍然没有开槽,因此它需要对类进行字典查找,因此调用它必然比调用 __str__
慢,但现在可以在格式化 int
的常见情况下完全避免调用或 str
实例(不是子类!)没有格式说明符。
【讨论】:
【参考方案2】:注意:此答案是为 Python 3.6 alpha 版本编写的。 new opcode added to 3.6.0b1 显着提高了 f-string 性能。
f"..."
语法在 ...
表达式周围的文字字符串部分上有效地转换为 str.join()
操作,并且表达式本身的结果通过 object.__format__()
方法传递(传递任何 :..
格式规范)。拆机的时候可以看到这个:
>>> import dis
>>> dis.dis(compile('f"X is x"', '', 'exec'))
1 0 LOAD_CONST 0 ('')
3 LOAD_ATTR 0 (join)
6 LOAD_CONST 1 ('X is ')
9 LOAD_NAME 1 (x)
12 FORMAT_VALUE 0
15 BUILD_LIST 2
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 POP_TOP
22 LOAD_CONST 2 (None)
25 RETURN_VALUE
>>> dis.dis(compile('"X is ".format(x)', '', 'exec'))
1 0 LOAD_CONST 0 ('X is ')
3 LOAD_ATTR 0 (format)
6 LOAD_NAME 1 (x)
9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
12 POP_TOP
13 LOAD_CONST 1 (None)
16 RETURN_VALUE
请注意该结果中的 BUILD_LIST
和 LOAD_ATTR .. (join)
操作码。新的FORMAT_VALUE
将堆栈顶部加上一个格式值(在编译时解析出来)将它们组合到object.__format__()
调用中。
所以你的例子,f"X is x"
,被翻译成:
''.join(["X is ", x.__format__('')])
请注意,这需要 Python 创建一个列表对象,并调用 str.join()
方法。
str.format()
调用也是一个方法调用,解析后还是涉及到x.__format__('')
调用,但关键是这里没有创建列表。正是这种差异使str.format()
方法更快。
请注意,Python 3.6 仅作为 alpha 版本发布;这个实现仍然可以很容易地改变。请参阅 PEP 494 – Python 3.6 Release Schedule 获取时间表,以及 Python issue #27078(针对此问题打开)讨论如何进一步提高格式化字符串文字的性能。
【讨论】:
非常好的解释,谢谢!我也不知道有一个__format__
魔术方法。
为什么扩展为''.join([...])
而不是字符串拼接?
@AlexHall:因为字符串连接具有 O(N^2) 性能特征。 A + B + C 必须先为 A + B 创建一个字符串,然后将结果与 C 一起复制到一个新字符串中。
@AlexHall:另一方面,字符串连接只需要计算最终的字符串大小,将所有 A、B 和 C 复制到其中。这是一个 O(N) 操作。
@AlexHall:字符串构建器仍然需要分块分配内存以为额外数据腾出空间,然后生成最终的字符串对象。两种方法都需要权衡取舍。当您使用 stringvar += otherstring
或 stringvar = stringvar + otherstring
时,CPython 确实有一个内部优化,但这是一个实现细节,也需要重新设计以支持这种情况,因为这里没有实际的 stringvar
。以上是关于为啥在 Python 3.6 alpha 中文字格式的字符串(f 字符串)如此缓慢? (现在在 3.6 稳定版中修复)的主要内容,如果未能解决你的问题,请参考以下文章