Бенчмарк двух реализаций, создающих много временных объектов, показывает преимущество одной из них только в timeit: какой механизм нужно проверить первым?
Сначала проверьте, что timeit по умолчанию временно отключает автоматический сбор циклического мусора во время измерения. Поэтому реализация, создающая циклические ссылки или большое количество объектов, может выглядеть искусственно быстрой: сборщик не тратит время на работу, а недостижимые циклы накапливаются.
Для честного сравнения нужно либо явно включить сборку мусора в измерении, либо воспроизвести условия эксплуатации приложения, где сборщик работает с обычными порогами.
Модуль timeit предназначен для измерения небольших фрагментов кода, где случайные паузы сборщика мусора могут заметно исказить результат. Поэтому он отключает автоматический сбор циклического мусора на время измерения, уменьшая шум и повышая воспроизводимость коротких тестов.
Это решение удобно для сравнения чистой стоимости операций, но не является универсальной моделью работы долгоживущего сервиса. В реальном приложении сборщик обычно включён, а его запуск влияет на задержки, пропускную способность и потребление памяти.
Если две реализации создают только обычные временные объекты без циклических ссылок, подсчёт ссылок в CPython часто освобождает их сразу, и отключение циклического сборщика может почти ничего не изменить. Но при наличии циклов недостижимые объекты не освобождаются одним подсчётом ссылок.
В таком тесте отключённый сборщик даёт сразу два искажения: измеряемое время может стать меньше, а потребление памяти — временно вырасти. Можно выбрать более быстрый вариант, который в сервисе будет периодически вызывать длительные сборки мусора или упираться в лимиты памяти.
timeit отключает GC, то есть сборщик циклических ссылок, но не отключает подсчёт ссылок и не отменяет создание объектов. Объекты без циклов по-прежнему обычно освобождаются механизмом подсчёта ссылок, а циклические структуры остаются до запуска соответствующего поколения сборщика.
Минимальная проверка выглядит так:
В первом измерении циклы обычно не собираются до завершения замера. Во втором сборщик включён, поэтому результат отражает стоимость его работы; конкретная разница зависит от порогов поколений, версии Python, типа создаваемых объектов и нагрузки.
Важно не делать вывод по одному запуску. Используйте несколько повторов, проверяйте разброс и измеряйте не только время, но и пиковое потребление памяти. Если целевая программа не создаёт циклов, включение GC может не объяснить разницу; тогда нужно искать другие причины, например различия в аллокациях, кэшировании или внешнем шуме.
Команда сравнивала два способа построения графа объектов. Первый создавал меньше временных списков, но оба варианта формировали циклические связи. В timeit первый вариант был быстрее, однако в длительном процессе его преимущество исчезало: периодические сборки поколений приводили к паузам, а тест с отключённым GC вообще показывал нереалистичный рост памяти.
Рассматривались три подхода: оставить стандартный режим timeit, включить GC через настройку измерения или сразу запускать полноценный тестовый сервис. Первый вариант был быстрым, но не отражал эксплуатацию; третий был наиболее реалистичным, но слишком дорогим для каждой итерации разработки.
Выбрали оба уровня проверки: короткий бенчмарк с явно включённым GC для локального сравнения и длительный сценарный тест с рабочими порогами сборщика. Это позволило отдельно оценить стоимость алгоритма и последствия его поведения в процессе, не смешивая эти вопросы.
Вопрос: Отключает ли timeit подсчёт ссылок вместе со сборщиком циклов?
Ответ: Нет. В CPython подсчёт ссылок продолжает работать. Отключается автоматический механизм обнаружения циклического мусора, а не сама возможность уменьшать счётчик ссылок. Поэтому временная строка или список без цикла обычно освобождается как прежде, а самоссылочный список остаётся до запуска циклического GC.
Вопрос: Достаточно ли включить GC один раз перед вызовом timeit?
Ответ: Нет, если используется обычный режим timeit: перед измерением он сам временно отключает GC. Состояние нужно изменить внутри измеряемого контекста, например через gc.enable() в setup или в обёртке, учитывая, что стоимость такой обёртки тоже может попасть в замер. После завершения timeit восстанавливает исходное состояние сборщика.
Вопрос: Почему одинаковое время при включённом и отключённом GC не доказывает отсутствие проблем со сборкой мусора?
Ответ: Нагрузка может не успеть создать достаточно объектов для достижения порогов сборки, а часть объектов может освобождаться подсчётом ссылок. Кроме того, короткий тест не показывает редкие паузы старших поколений и накопление долгоживущих структур. Поэтому отсутствие разницы в микробенчмарке нужно проверять длительным тестом, статистикой запусков GC и наблюдением за временем ответа и памятью.