Внутренние детали конкатенации строк Python - PullRequest
6 голосов
/ 08 марта 2019

Предположим, что у нас есть список строк, и мы хотим создать строку путем объединения всех элементов в этом списке.Примерно так:

def foo(str_lst):
    result = ''
    for element in str_lst:
        result += element
    return result

Поскольку строки являются неизменяемыми объектами, я ожидаю, что python создает новый объект str и копирует содержимое результата и элемента на каждой итерации.Это усложняет O(M * N^2) время, M - длину каждого элемента, а N - размер списка.

Однако мой эксперимент показывает, что он выполняется за линейное время.

N = 1000000 # 1 million
str_lst = ['a' for _ in range(N)]

foo(str_lst) # It takes around 0.5 seconds

N = 2000000 # 2 million
str_lst = ['a' for _ in range(N)]

foo(str_lst) # It takes around 1.0 seconds

N = 10000000 # 10 million
str_lst = ['a' for _ in range(N)]

foo(str_lst) # It takes around 5.3 seconds

Я подозреваю, что python использует что-то вроде stringbuffer под капотом.Таким образом, он не создает новый объект на каждой итерации.

Теперь рассмотрим немного другую реализацию.Разница лишь в одном дополнительном назначении.

def foo2(str_lst):
    result = ''
    for element in str_lst:
        result += element
        temp = result # new added line
    return result

Я знаю, что строка temp = result не создает новый объект.temp просто указывает на тот же объект.Таким образом, это небольшое изменение не должно сильно повлиять на производительность.

N = 1000000 # 1 million
str_lst = ['a' for _ in range(N)]
foo(str_lst) # It takes around 0.5 seconds
foo2(str_lst) # It takes around 30 seconds

N = 2000000 # 2 million
str_lst = ['a' for _ in range(N)]
foo(str_lst) # It takes around 1 seconds
foo2(str_lst) # It takes around 129 seconds

Однако есть огромная разница.И похоже, что функция foo2 - это O (N ^ 2), а функция foo - это O (N).

Мой вопрос: как python достигает линейного времени в конкатенации строк, не нарушая другие языковые компоненты, такие как неизменяемое присвоение объектов?и как эта дополнительная строка так сильно влияет на производительность?Я немного искал в реализации cpython, но не смог найти точное местоположение.

Обновление

Вот результаты профилирования линии.

результат для функции foo

Total time: 0.545577 s
File: <ipython-input-38-b9bb169e8fe0>
Function: foo at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
 1                                           def foo(str_lst):
 2         1          2.0      2.0      0.0      result = ''
 3   1000001     238820.0      0.2     43.8      for element in str_lst:
 4   1000000     306755.0      0.3     56.2          result += element
 5         1          0.0      0.0      0.0      return result

результат для функции foo2

Total time: 30.6663 s
File: <ipython-input-40-34dd53670dd9>
Function: foo2 at line 1
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
 1                                           def foo2(str_lst):
 2         1          2.0      2.0      0.0      result = ''
 3   1000001     299122.0      0.3      1.0      for element in str_lst:
 4   1000000   30033127.0     30.0     97.7          result += element
 5   1000000     413283.0      0.4      1.3          temp = result
 6         1          0.0      0.0      0.0      return result

Каким-то образом строка temp = result влияет на производительность линии result += element.

Ответы [ 2 ]

6 голосов
/ 08 марта 2019

Наличие другого имени, указывающего на тот же объект, убивает оптимизацию. Оптимизация в основном работает путем изменения размера строкового объекта и добавления его на место. Если у вас есть более одной ссылки на этот объект, вы не можете изменить размер, не затрагивая другую ссылку. Поскольку строки являются неизменяемыми, это может стать серьезным недостатком реализации.

temp = result

увеличил количество ссылок для строкового объекта, названного result, тем самым запретив оптимизацию.

Полный список проверок, выполненных в случае += (что в конечном итоге приводит к PyUnicode_Append), можно увидеть в функции unicode_modifiable. Помимо прочего, он проверяет, что счетчик ссылок объекта равен единице, что он не интернирован и не является подклассом строки.

Есть еще пара проверок в операторе if , защищающем эту оптимизацию, если вы хотите более подробный список.


Хотя это не основная проблема вашего вопроса, будущим читателям может быть интересно узнать, как эффективно выполнять конкатенацию строк. Помимо подобных вопросов по S.O, в Python FAQ также есть запись по этому вопросу.

3 голосов
/ 08 марта 2019

На самом деле поведение, которое вы наблюдаете, определяется поведением распределителя памяти среды выполнения C в вашей ОС.

CPython имеет оптимизацию, что если объект unicode имеет только одну ссылку, его можно изменить на месте - никто не зарегистрирует, что объект unicode на мгновение потеряет свою неизменность.См. Мой ответ на этот SO-вопрос для получения более подробной информации.

В foo2 есть еще одна ссылка на объект Unicode (temp), которая предотвращаетоптимизация: изменение его на месте нарушило бы неизменность, потому что это можно наблюдать через temp.

Однако даже при оптимизации на месте не очевидно, почему можно избежать поведения O(n^2),так как объект Unicode не перераспределяется и, следовательно, должен расширять базовый буфер при каждом добавлении, что наивно означало бы копию всего содержимого (т.е. O(n)) на каждом шаге.

Однако большинство извремя realloc (отличное от malloc + копирование) может быть выполнено в O(1), поскольку, если память непосредственно за выделенным буфером свободна, ее можно использовать для расширения оригинала без копирования.


Интересная деталь в том, что нет гарантии, что foo будет работать в O(n): если память испорчена (например, в длительном процессе).realloc не сможет расширить буфер без копирования данных, и, следовательно, время выполнения станет O(n^2).

Таким образом, не следует полагаться на эту оптимизацию, чтобы избежать квадратичного времени работы.

...