Почему литеральные отформатированные строки (f-строки) были такими медленными в альфа-версии Python 3.6? (теперь исправлено в стабильной версии 3.6)

Я загрузил альфа-версию Python 3.6 из репозитория Python Github, и одной из моих любимых новых функций является буквальное форматирование строк. Его можно использовать так:

>>> x = 2
>>> f"x is {x}"
"x is 2"

Похоже, это делает то же самое, что и использование функции format для экземпляра str. Однако одна вещь, которую я заметил, заключается в том, что это буквальное форматирование строки на самом деле очень медленное по сравнению с простым вызовом 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 занимает почти половину времени. Я ожидаю, что литеральный метод будет быстрее, потому что задействовано меньше синтаксиса. Что происходит за кулисами, из-за чего буквальный метод работает намного медленнее?


person Aaron Christiansen    schedule 21.05.2016    source источник
comment
f-строки являются динамическими, поэтому строка должна генерироваться в каждом цикле; тогда как строка формата является литералом, который создается до запуска кода, когда он преобразуется в байт-код.   -  person PM 2Ring    schedule 21.05.2016
comment
@AlexHall Возможно, это связано с тем фактом, что x присваивается локальной переменной при передаче в метод format, но ее нужно найти в globals по синтаксису f"...".   -  person schwobaseggl    schedule 21.05.2016
comment
@AlexHall: это не ошибка. Просто под капотом находится другая реализация, поскольку строка формата должна анализироваться во время компиляции, тогда как str.format() анализирует слоты во время runtime.   -  person Martijn Pieters    schedule 21.05.2016
comment
@PM2Ring: все выражения компилируются во время компиляции и оцениваются во время выполнения.   -  person Martijn Pieters    schedule 21.05.2016
comment
@MartijnPieters, если строка компилируется во время выполнения, это должно означать меньше вычислений. По крайней мере, если .format быстрее, то эти строки должны быть просто скомпилированы в вызовы .format.   -  person Alex Hall    schedule 21.05.2016
comment
Обратите внимание, что ваш .format самый простой из возможных. Он даже не использует kwargs или даже индексированный доступ. Как только вы используете '{x}'.format(x=x), разница исчезает.   -  person Antti Haapala    schedule 21.05.2016
comment
@AnttiHaapala Даже при использовании этого метода строки в буквальном формате все еще медленнее, но меньше.   -  person Aaron Christiansen    schedule 21.05.2016


Ответы (2)


Примечание. Этот ответ был написан для альфа-версий Python 3.6. новый код операции, добавленный в 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__(''), но, что особенно важно, здесь не задействовано создание списка. Именно эта разница делает метод str.format() быстрее.

Обратите внимание, что Python 3.6 был выпущен только в виде альфа-сборки; эта реализация все еще может быть легко изменена. См. PEP 494 — Расписание выпуска Python 3.6. расписание, а также Python issue #27078 (открыт в ответ на этот вопрос) для обсуждения как еще больше повысить производительность форматированных строковых литералов.

person Martijn Pieters    schedule 21.05.2016
comment
Действительно хорошее объяснение, спасибо! Я и понятия не имел, что существует __format__ магический метод. - person Aaron Christiansen; 21.05.2016
comment
Почему он расширяется до ''.join([...]), а не до конкатенации строк? - person Alex Hall; 21.05.2016
comment
@AlexHall: потому что конкатенация строк имеет характеристики производительности O (N ^ 2). A + B + C должны сначала создать строку для A + B, а затем скопировать результат вместе с C в новую строку. - person Martijn Pieters; 21.05.2016
comment
@AlexHall: с другой стороны, при объединении строк нужно только рассчитать окончательный размер строки, скопировать в него все A, B и C. Это операция O(N). - person Martijn Pieters; 21.05.2016
comment
Это не должно быть таким. Java, например, расширяет простую конкатенацию строк до вызовов StringBuilder. Дело в том, что должен быть способ статически скомпилировать конкатенацию строк во что-то, что не требует создания списка. - person Alex Hall; 21.05.2016
comment
@AlexHall: построителю строк по-прежнему необходимо выделять память по частям, чтобы освободить место для дополнительных данных, а затем создавать окончательный строковый объект. Есть компромиссы для обоих подходов. CPython имеет внутреннюю оптимизацию, когда вы используете stringvar += otherstring или stringvar = stringvar + otherstring, но это деталь реализации, которая потребует переделки для поддержки и этого случая, поскольку здесь нет фактического stringvar. - person Martijn Pieters; 21.05.2016
comment
Хорошо, скажи так. Что делает .format, не используя список, и почему бы просто не сделать это? - person Alex Hall; 21.05.2016
comment
@AlexHall: это вполне может быть вариантом; создайте строковую константу с выражениями, замененными позиционными аргументами, найдите для нее метод .format() и передайте результаты выражения с опкодом CALL_FUNCTION. Это требует, чтобы строка анализировалась каждый раз, когда вы это делаете. Я не знаю, был ли здесь сделан преднамеренный выбор на основе метрик строк большого формата или чего-то подобного. - person Martijn Pieters; 21.05.2016
comment
Возможно, стоит отметить в этом ответе, что проблема с тех пор была решена. - person ideasman42; 05.01.2017
comment
@ideasman42: ну, не в альфа-версии и бета-версии 1 ;-) В финальном выпуске производительность значительно улучшилась, да. - person Martijn Pieters; 05.01.2017

До версии 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 для фрагментов строки в строку. (опкод получает один операнд - количество фрагментов в стеке; фрагменты помещаются в стек в порядке появления, т.е. последняя часть является самым верхним элементом). Сергей Сторчака реализовал этот новый код операции и объединил его с CPython, так что он был доступен в Python 3.6 начиная с бета-версии 1 (и, следовательно, в финальной версии Python 3.6.0).

В результате строки с литеральным форматированием будут намного быстрее, чем string.format. Они также часто намного быстрее, чем форматирование в старом стиле в Python 3.6, если вы просто интерполируете объекты str или int:

>>> 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 полностью устраняет первые 5 из 6 источников неэффективности. Метод __format__ по-прежнему не имеет слотов, поэтому он требует поиска по словарю в классе, поэтому его вызов обязательно медленнее, чем вызов __str__, но теперь можно полностью избежать вызова в обычных случаях форматирования экземпляров int или str (не подклассов). !) без спецификаторов форматирования.

person Antti Haapala    schedule 06.09.2016