Построение строки путем повторной конкатенации строк является анти-шаблоном, но мне все же интересно, почему его производительность переключается с линейного на квадратичный после длины строки, превышающей приблизительно 10 ** 6:
# this will take time linear in n with the optimization
# and quadratic time without the optimization
import time
start = time.perf_counter()
s = ''
for i in range(n):
s += 'a'
total_time = time.perf_counter() - start
time_per_iteration = total_time / n
Например, на моей машине (Windows 10, python 3.6.1):
- для
10 ** 4 < n < 10 ** 6
,time_per_iteration
почти идеально постоянна при 170 ± 10 мкс - для
10 ** 6 < n
,time_per_iteration
почти идеально линейна, достигая 520 мкс приn == 10 ** 7
.
Линейный рост в time_per_iteration
эквивалентен квадратичному росту в total_time
.
Линейная сложность получается из optimization в более поздних версиях CPython (2.4+), которые повторно использовать исходное хранилище, если ссылки на исходный объект не оставлены. Но я ожидал, что линейная производительность будет продолжаться бесконечно, а не переключиться на квадратичную в какой-то момент.
Мой вопрос основан на этом комментарии. По какой-то нечетной причине запуск
python -m timeit -s"s=''" "for i in range(10**7):s+='a'"
занимает невероятно долгое время (гораздо длиннее квадратичного), поэтому я никогда не получал фактические результаты времени от timeit
. Поэтому вместо этого я использовал простой цикл, как указано выше, для получения номеров производительности.
Update:
Мой вопрос также может быть озаглавлен "Как может показаться append
в списках append
без перераспределения?". Из наблюдаемой константы time_per_iteration
для малогабаритных строк я предположил, что оптимизация строк должна быть чрезмерно распределена. Но realloc
(неожиданно для меня) довольно успешно избегает копирования памяти при расширении небольших блоков памяти.