为什么Python 3.6 alpha中的文字格式化字符串如此之慢?(现在固定在3.6稳定)

Aar*_*sen 32 python performance python-internals python-3.6 f-string

我从Python Github存储库下载了一个Python 3.6 alpha版本,我最喜欢的一个新功能是文字字符串格式化.它可以像这样使用:

>>> x = 2
>>> f"x is {x}"
"x is 2"
Run Code Online (Sandbox Code Playgroud)

这似乎与formatstr实例上使用该函数做同样的事情.但是,我注意到的一件事是,与仅调用相比,这种文字字符串格式化实际上非常慢format.以下是timeit关于每种方法的内容:

>>> x = 2
>>> timeit.timeit(lambda: f"X is {x}")
0.8658502227130764
>>> timeit.timeit(lambda: "X is {}".format(x))
0.5500578542015617
Run Code Online (Sandbox Code Playgroud)

如果我使用字符串作为timeit参数,我的结果仍然显示模式:

>>> timeit.timeit('x = 2; f"X is {x}"')
0.5786435347381484
>>> timeit.timeit('x = 2; "X is {}".format(x)')
0.4145195760771685
Run Code Online (Sandbox Code Playgroud)

如您所见,使用format几乎占用了一半的时间.我希望文字方法更快,因为涉及的语法更少.幕后发生了什么导致文字方法如此慢?

Mar*_*ers 32

注意:这个答案是为Python 3.6 alpha版本编写的.一个新的操作码添加到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
Run Code Online (Sandbox Code Playgroud)

请注意该结果中的BUILD_LISTLOAD_ATTR .. (join)op代码.new FORMAT_VALUE采用堆栈顶部加上格式值(在编译时解析)以在object.__format__()调用中组合这些值.

所以你的例子f"X is {x}"被翻译成:

''.join(["X is ", x.__format__('')])
Run Code Online (Sandbox Code Playgroud)

请注意,这需要Python创建一个列表对象,并调用该str.join()方法.

str.format()调用也是一个方法调用,在解析后仍然有一个x.__format__('')涉及的调用,但关键的是,这里没有涉及列表创建.正是这种差异使得该str.format()方法更快.

请注意,Python 3.6仅作为alpha版本发布; 这个实现仍然可以轻松改变.有关如何进一步提高格式化字符串文字性能的讨论,请参阅时间表的 PEP 494 - Python 3.6发布时间表,以及Python问题#27078(对此问题的响应打开).

  • @AlexHall:因为字符串连接具有O(N ^ 2)性能特征.A + B + C必须首先为A + B创建一个字符串,然后将结果与C一起复制到一个新字符串. (3认同)
  • @AlexHall:另一方面,字符串连接只需要计算最终的字符串大小,将所有 A、B 和 C 复制到其中。这是一个 O(N) 操作。 (2认同)

Ant*_*ala 22

在3.6 beta 1之前,格式字符串f'x is {x}'被编译为相当于''.join(['x is ', x.__format__('')]).由于以下几个原因,生成的代码效率低下:

  1. 它构建了一系列字符串片段......
  2. ......这个序列是一个列表,而不是一个元组!(构造元组比列表稍快一些).
  3. 它将空字符串推入堆栈
  4. join在空字符串上查找方法
  5. __format__甚至在裸的Unicode对象上调用,对象__format__('')总是会返回self,或整数对象,__format__('')作为参数返回str(self).
  6. __format__ 方法没有插入.

但是,对于更复杂和更长的字符串,文字格式化的字符串仍然比相应的'...'.format(...)调用更快,因为对于后者,每次格式化字符串时都会解释字符串.


这个问题是问题27078的主要动机,要求将字符串片段的新Python字节码操作码转换为字符串(操作码获取一个操作数 - 堆栈中的片段数量;片段以相反的顺序推入,即最后一个部分是最重要的项目).Serhiy Storchaka实现了这个新的操作码并将其合并到CPython中,因此自从beta 1版本(因此在Python 3.6.0 final)中它已经在Python 3.6中可用.

作为结果的文字格式的字符串会快于string.format.如果你只是插值或对象,它们通常也比Python 3.6中的旧式格式快得多:strint

>>> 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

新的BUILD_STRING,以及FORMAT_VALUE代码优化完全消除了6个效率低下的前5个.该__format__方法仍然没有插入,因此它需要在类上进行字典查找,因此调用它必然比调用慢__str__,但现在可以在格式化intstr实例(不是子类!)的常见情况下完全避免调用而不进行格式化符.