为什么文字格式化字符串(f 字符串)在 Python 3.6 alpha 中这么慢? (现在固定在 3.6 稳定)
Why were literal formatted strings (f-strings) so slow in Python 3.6 alpha? (now fixed in 3.6 stable)
我已经从 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
几乎花费了一半的时间。我希望文字方法更快,因为涉及的语法更少。导致文字方法如此慢的幕后原因是什么?
注意:此答案是为 Python 3.6 alpha 版本编写的。 new opcode added to 3.6.0b1 显着提高了 f 弦性能。
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__('')
,关键是没有list的创建 涉及到这里。正是这种差异使得 str.format()
方法更快。
请注意 Python 3.6 仅作为 alpha 版本发布;这个实现仍然可以很容易地改变。有关如何进一步提高格式化字符串文字性能的讨论,请参阅 PEP 494 – Python 3.6 Release Schedule for the time table, as well as Python issue #27078(针对此问题打开)。
在 3.6 beta 1 之前,格式字符串 f'x is {x}'
被编译为 ''.join(['x is ', x.__format__('')])
的等效项。由于以下几个原因,生成的字节码效率低下:
- 它构建了一系列字符串片段...
- ... 这个序列是一个列表,而不是一个元组! (构造元组比构造列表稍微快一些)。
- 它将一个空字符串压入堆栈
- 它在空字符串上查找了
join
方法
- 它甚至在裸 Unicode 对象上调用
__format__
,__format__('')
总是 return self
,或者整数对象,__format__('')
作为参数 returned str(self)
.
__format__
方法未开槽
然而,对于更复杂和更长的字符串,文字格式化字符串仍然比相应的 '...'.format(...)
调用更快,因为对于后者,每次格式化字符串时都会解释字符串。
这个问题正是 issue 27078 寻求新的 Python 字节码操作码的主要动机,用于将字符串片段转换为字符串(操作码获得一个操作数 - 堆栈上的片段数;片段按出现顺序被推入堆栈,即最后一部分是最顶层的项目)。 Serhiy Storchaka 实现了这个新的操作码并将其合并到 CPython 中,因此自 beta 1 版本以来它就在 Python 3.6 中可用(因此在 Python 3.6.0 final 中)。
因此,文字格式化字符串 比 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__
方法仍然没有槽,所以它需要在 class 上进行字典查找,因此调用它必然比调用 __str__
慢,但现在可以完全避免调用格式化 int
或 str
实例的常见情况(不是 subclasses!)没有格式化说明符。
我已经从 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
几乎花费了一半的时间。我希望文字方法更快,因为涉及的语法更少。导致文字方法如此慢的幕后原因是什么?
注意:此答案是为 Python 3.6 alpha 版本编写的。 new opcode added to 3.6.0b1 显着提高了 f 弦性能。
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__('')
,关键是没有list的创建 涉及到这里。正是这种差异使得 str.format()
方法更快。
请注意 Python 3.6 仅作为 alpha 版本发布;这个实现仍然可以很容易地改变。有关如何进一步提高格式化字符串文字性能的讨论,请参阅 PEP 494 – Python 3.6 Release Schedule for the time table, as well as Python issue #27078(针对此问题打开)。
在 3.6 beta 1 之前,格式字符串 f'x is {x}'
被编译为 ''.join(['x is ', x.__format__('')])
的等效项。由于以下几个原因,生成的字节码效率低下:
- 它构建了一系列字符串片段...
- ... 这个序列是一个列表,而不是一个元组! (构造元组比构造列表稍微快一些)。
- 它将一个空字符串压入堆栈
- 它在空字符串上查找了
join
方法 - 它甚至在裸 Unicode 对象上调用
__format__
,__format__('')
总是 returnself
,或者整数对象,__format__('')
作为参数 returnedstr(self)
. __format__
方法未开槽
然而,对于更复杂和更长的字符串,文字格式化字符串仍然比相应的 '...'.format(...)
调用更快,因为对于后者,每次格式化字符串时都会解释字符串。
这个问题正是 issue 27078 寻求新的 Python 字节码操作码的主要动机,用于将字符串片段转换为字符串(操作码获得一个操作数 - 堆栈上的片段数;片段按出现顺序被推入堆栈,即最后一部分是最顶层的项目)。 Serhiy Storchaka 实现了这个新的操作码并将其合并到 CPython 中,因此自 beta 1 版本以来它就在 Python 3.6 中可用(因此在 Python 3.6.0 final 中)。
因此,文字格式化字符串 比 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__
方法仍然没有槽,所以它需要在 class 上进行字典查找,因此调用它必然比调用 __str__
慢,但现在可以完全避免调用格式化 int
或 str
实例的常见情况(不是 subclasses!)没有格式化说明符。