Почему ''.join() быстрее, чем += в Python?
Я могу найти множество информации в Интернете (о переполнении стека и т. Д.) О том, что это очень неэффективная и плохая практика использования +
или же +=
для конкатенации в Python.
Я не могу найти, почему +=
это так неэффективно. Помимо упоминания здесь, что "оно было оптимизировано для улучшения на 20% в определенных случаях" (все еще неясно, что это за случаи), я не могу найти никакой дополнительной информации.
Что происходит на более техническом уровне, что делает ''.join()
превосходит другие методы конкатенации Python?
2 ответа
Допустим, у вас есть этот код для построения строки из трех строк:
x = 'foo'
x += 'bar' # 'foobar'
x += 'baz' # 'foobarbaz'
В этом случае Python сначала должен выделить и создать 'foobar'
прежде чем он может выделить и создать 'foobarbaz'
,
Так что для каждого +=
при вызове все содержимое строки и все, что к ней добавляется, необходимо скопировать в совершенно новый буфер памяти. Другими словами, если у вас есть N
Строки, которые нужно соединить, нужно выделить примерно N
временные строки и первая подстрока копируется ~N раз. Последняя подстрока копируется только один раз, но в среднем каждая подстрока копируется ~N/2
раз.
С .join
, Python может сыграть несколько трюков, так как промежуточные строки создавать не нужно. CPython вычисляет, сколько памяти ему нужно заранее, а затем выделяет буфер правильного размера. Наконец, он затем копирует каждый фрагмент в новый буфер, что означает, что каждый фрагмент копируется только один раз.
Существуют и другие жизнеспособные подходы, которые могут привести к повышению +=
в некоторых случаях. Например, если внутреннее строковое представление на самом деле rope
или если среда выполнения на самом деле достаточно умна, чтобы каким-то образом выяснить, что временные строки не нужны программе, и оптимизировать их.
Тем не менее, CPython, безусловно,не выполняет эти оптимизации надежно (хотя это может быть сделано в нескольких угловых случаях), и поскольку это наиболее распространенная используемая реализация, многие передовые практики основаны на том, что хорошо работает для CPython. Наличие стандартизированного набора норм также позволяет другим реализациям сосредоточить свои усилия по оптимизации.
Я думаю, что такое поведение лучше всего объяснить в главе о буфере строк Lua.
Чтобы переписать это объяснение в контексте Python, давайте начнем с невинного фрагмента кода (производного от кода в документации Lua):
s = ""
for l in some_list:
s += l
Предположим, что каждый l
20 байтов и s
уже был проанализирован до размера 50 кб. Когда Python соединяется s + l
он создает новую строку с 50 020 байтами и копирует 50 КБ из s
в эту новую строку. То есть для каждой новой строки программа перемещает 50 КБ памяти и продолжает расти. После прочтения 100 новых строк (всего 2 КБ) фрагмент уже переместил более 5 МБ памяти. Что еще хуже, после назначения
s += l
старая строка теперь мусор. После двух циклов цикла две старые строки составляют более 100 КБ мусора. Итак, языковой компилятор решает запустить сборщик мусора и освобождает эти 100 КБ. Проблема в том, что это будет происходить каждые два цикла, и программа будет запускать сборщик мусора две тысячи раз, прежде чем читать весь список. Даже со всей этой работой, использование памяти будет многократно превышать размер списка.
И, в конце концов:
Эта проблема не характерна для Lua: другие языки с истинной сборкой мусора, где строки являются неизменяемыми объектами, демонстрируют аналогичное поведение, наиболее известным примером является Java. (Java предлагает структуру
StringBuffer
чтобы улучшить проблему.)
Строки Python также являются неизменяемыми объектами.