Почему join быстрее, чем обычная конкатенация

Я видел несколько примеров из разных языков, которые однозначно доказывают, что объединение элементов списка(массива) в разы быстрее, чем просто объединение строки. К сожалению, я не нашел объяснения почему? Может ли кто-нибудь объяснить внутренний алгоритм, который работает под обеими операциями и почему один быстрее другого.

вот пример, что я имею в виду:

# This is slow
x = 'a'
x += 'b'
...
x += 'z'

# This is fast
x = ['a', 'b', ... 'z']
x = ''.join(x)

спасибо заранее )

7 ответов


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

с другой стороны, одна операция += в строке не имеет выбора, кроме как просто выделить достаточно памяти для окончательного строку, которая является конкатенацией двух строк. Последующие += должны делать то же самое, каждая выделяя память, которая на следующем += будет отброшена. Каждый раз, когда постоянно растущая строка копируется из одного места в памяти в другое.


причина в том, что строки в Python (и многих других языках)неизменяемые объекты - то есть, после создания, они не могут быть изменены. Вместо этого объединение строки фактически делает новая строка, которая состоит из содержимого двух меньших строк, которые объединяются, а затем заменяет старую строку новой.

Так как создание строки занимает определенное количество времени (необходимо выделить память, скопируйте содержимое строки в эта память и т. д.), Создание многих строк занимает больше времени, чем создание одной строки. Делать N конкатенации требует создания N новые строки в процессе. join(), с другой стороны, нужно создать только одну строку (конечный результат) и, таким образом, работает намного быстрее.


это потому, что для конкатенации строк должен выделяться все больший и больший кусок памяти:

x = 'a' # String of size 1 allocated
x += 'b' # String of size 2 allocated, x copied, and 'b' added. Old x discarded
x += 'b' # String of size 3 allocated, x copied, and 'c' added. Old x discarded
x += 'b' # String of size 4 allocated, x copied, and 'd' added. Old x discarded
x += 'b' # String of size 5 allocated, x copied, and 'e' added. Old x discarded

Итак, вы выполняете большие распределения и копии,но затем поворачиваетесь и выбрасываете их. Очень расточительно.

x = ['a', 'b', ..., 'z'] # 26 small allocations
x = ''.join(x) # A single, large allocation

посмотреть python string join performance и один конкретный anwser, который описывает его очень хорошо:

совет заключается в объединении многих строк.

для вычисления s = s1 + s2 + ... + СН

1) через +. Создается новая строка s1+s2, затем создается новая строка s1+s2+s3... и т. д., Поэтому задействовано много операций выделения и копирования памяти. Фактически, S1 копируется n-1 раз, s2 копируется n-2 раз,..., так далее.

2) используя "".соединение ([s1, s2,...,олово.)] Конкатенация выполняется за один проход, и каждый символ в строках копируется только один раз.


другие ответы в основном охватывали его, но если вы хотите еще больше деталей, у Джоэла Спольски есть статья, в которой он описывает "алгоритм художника Шлемиля", что чрезвычайно актуально и прекрасно объясняет, почему понимание такого рода деталей реализации низкого уровня по-прежнему очень важно, даже если вы работаете на языке высокого уровня, таком как Python.


Ну, это сильно зависит от языка, но в целом идея есть, что одна большая операция быстрее, чем многие маленькие. Во втором примере соединение знает все элементы, к которым оно должно присоединиться, и поэтому может просто выделить необходимые ресурсы и поместить символы. Конкатенация в первом примере должна перераспределять ресурсы на каждом шаге (в худшем случае).


Я не знаю внутренних элементов join, но в первой версии вы создаете новую строку каждый раз, когда вызываете оператор+=. Поскольку строки неизменяемы, каждый раз, когда выделяется новая память и создается копия.

теперь join (который является строковым методом) может выполнять только одно распределение, так как он может вычислить размер заранее.