【问题标题】: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 的调用。

标签: python performance python-internals python-3.6 f-string


【解决方案1】:

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

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

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


这个问题是issue 27078 要求新的 Python 字节码操作码以将字符串片段转换为字符串的主要动机(操作码获取一个操作数 - 堆栈上的片段数;片段被推送到堆栈中出现的顺序,即最后一部分是最上面的项目)。 Serhiy Storchaka 实现了这个新的操作码并将其合并到 CPython 中,这样它就可以在 Python 3.6 中使用,从 beta 1 版本开始(因此在 Python 3.6.0 最终版本中)。

因此,文字格式的字符串将比string.format很多。如果您只是插入 strint 对象,它们通常也比 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_LISTLOAD_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 += otherstringstringvar = stringvar + otherstring 时,CPython 确实有一个内部优化,但这是一个实现细节,也需要重新设计以支持这种情况,因为这里没有实际的 stringvar
    猜你喜欢
    • 2016-12-29
    • 1970-01-01
    • 1970-01-01
    • 2016-12-10
    • 2021-03-25
    • 2016-06-15
    • 1970-01-01
    • 2018-01-03
    相关资源
    最近更新 更多