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