Замер времени в Python: чем измерять и где ошибаются
time.time() врёт при переводе часов, а timeit запускает код тысячи раз не просто так. Разбираем, каким инструментом мерить что и почему разброс замеров — не шум, а информация.

Вы замерили функцию через time.time(), получили 0.003 секунды, запустили ещё раз — вышло 0.011. Разброс в три раза, хотя код не менялся. Дальше начинается угадайка: то ли код медленный, то ли соседний процесс отъел ядро, то ли сам замер кривой. В Python есть отдельные инструменты под каждую задачу, и путаница между ними — самая частая причина бессмысленных цифр. Разбираем, чем мерить микрооперацию, чем — реальный запрос к базе, и почему один прогон почти всегда врёт.
Всё ниже — про CPython 3.8 и новее. Функция time.perf_counter() и модуль timeit в этих версиях стабильны, разница в поведении оговорена отдельно.
Три разных вопроса — три разных инструмента
Прежде чем звать функцию, определитесь, что именно вы хотите узнать. От этого зависит выбор.
- Сколько заняла операция один раз в реальном коде — запрос к API, чтение файла, обработка запроса. Здесь важно настенное время1 и один честный прогон. Инструмент:
time.perf_counter(). - Насколько быстра короткая операция сама по себе — сравнить два способа собрать строку, два способа отфильтровать список. Операция настолько быстрая, что один вызод не измерить. Инструмент:
timeit. - Где именно в большой функции уходит время — не «сколько всего», а «какая строка виновата». Инструмент:
cProfile, не ручной замер.
Смешивать их — типичная ошибка. Мерить запрос к базе через timeit бессмысленно: сеть даст такой разброс, что усреднение по тысяче прогонов ничего не покажет, зато нагрузит базу. Мерить скорость str.join через один perf_counter() тоже бесполезно — операция короче, чем погрешность таймера.
perf_counter вместо time.time
Главная ошибка новичков — брать time.time(). Эта функция возвращает системное время, которое может прыгнуть назад при синхронизации с NTP или переводе часов. Ваш замер тогда покажет отрицательную длительность или лишние секунды.
Для интервалов есть time.perf_counter() — монотонные часы с максимальным доступным разрешением. Они не привязаны к календарному времени и не прыгают.
import time
start = time.perf_counter()
result = do_expensive_work() # ваша реальная операция
elapsed = time.perf_counter() - start
print(f"Заняло {elapsed:.4f} с")
Если нужны наносекунды и вы хотите избежать погрешности от арифметики с float, начиная с Python 3.7 есть целочисленный вариант time.perf_counter_ns(), который возвращает целое число наносекунд:
import time
start = time.perf_counter_ns()
result = do_expensive_work()
elapsed_ns = time.perf_counter_ns() - start
print(f"Заняло {elapsed_ns / 1_000_000:.3f} мс")
time.time() отвечает на вопрос «который час», perf_counter() — на вопрос «сколько прошло». Это разные вопросы, и для замеров нужен второй.
Для замера именно процессорного времени, без учёта сна и ожидания ввода-вывода, есть time.process_time(). Если ваша функция полсекунды ждёт ответа сети, perf_counter покажет полсекунды, а process_time — почти ноль. Выбор зависит от того, что вы оптимизируете: настенное время пользователя или загрузку CPU.
timeit для коротких операций
Когда операция занимает микросекунды, один прогон измерить нельзя: разрешение таймера и накладные расходы на сам вызов больше самой операции. timeit решает это, прогоняя код много раз и деля общее время на число повторов.
Из командной строки это самый быстрый способ сравнить два подхода:
python -m timeit -s "items = list(range(1000))" "[x * 2 for x in items]"
python -m timeit -s "items = list(range(1000))" "list(map(lambda x: x * 2, items))"
Флаг -s — это setup: код, который выполнится один раз до замеров и в само время не попадёт. Основная строка — то, что повторяется. timeit сам подберёт число повторов так, чтобы суммарное время было осмысленным, и покажет лучший результат из нескольких серий.
Из кода интерфейс другой. Обратите внимание: timeit.timeit() возвращает суммарное время всех повторов, а не среднее — делить нужно самому:
import timeit
setup = "items = list(range(1000))"
stmt = "[x * 2 for x in items]"
total = timeit.timeit(stmt, setup=setup, number=100_000)
print(f"Один прогон: {total / 100_000 * 1e6:.3f} мкс")
Лучше пользоваться timeit.repeat() — он делает несколько серий и возвращает список. Из него берут минимум, а не среднее: минимальное время — это прогон, которому меньше всего мешала система, то есть ближайший к чистой скорости кода.
import timeit
results = timeit.repeat(stmt, setup=setup, repeat=5, number=100_000)
best = min(results) / 100_000
print(f"Лучший прогон: {best * 1e6:.3f} мкс")
Почему минимум, а не среднее: внешние помехи — планировщик ОС, сборщик мусора, другой процесс — только замедляют замер, ускорить не могут. Среднее размывается этими помехами, минимум их отсекает.
Сравнение инструментов
| Инструмент | Когда | Что измеряет | Подвох |
|---|---|---|---|
time.time() | никогда для интервалов | календарное время | прыгает при синхронизации часов |
time.perf_counter() | один прогон реальной операции | настенное время, монотонно | включает сон и ожидание I/O |
time.process_time() | нагрузка на CPU | процессорное время | не учитывает ожидание сети и диска |
timeit | короткие операции, сравнение | усреднённое по многим прогонам | бесполезен для I/O и долгих задач |
cProfile | поиск узкого места в функции | время по каждому вызову | сам замедляет код, цифры относительные |
Контекстный менеджер для повторяющихся замеров
Если в одном скрипте нужно замерить несколько блоков, удобно завернуть perf_counter в контекстный менеджер, чтобы не дублировать арифметику:
import time
from contextlib import contextmanager
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f} с")
# использование
with timer("загрузка данных"):
data = load_dataset()
with timer("обработка"):
result = process(data)
Блок finally здесь важен: если внутри with вылетит исключение, время всё равно выведется, и вы увидите, докуда код дошёл до падения.
Когда что выбирать
- Меряете реальную операцию с I/O (запрос, чтение файла, вызов API) —
perf_counter(), один прогон, при необходимости несколько раз вручную и посмотреть разброс. - Сравниваете две короткие чистые операции над данными в памяти —
timeit.repeat, берёте минимум. - Оптимизируете загрузку процессора в вычислениях без ожидания —
process_time(). - Не знаете, какая часть функции медленная, — не гадайте с ручными таймерами, запускайте
cProfile.
Что ломает замеры
Даже с правильным инструментом цифры врут по типичным причинам:
- Разогрев. Первый вызов функции часто медленнее: импорты, ленивая инициализация, кеши процессора холодные. Отбросьте первый прогон или сделайте один «пустой» до замера.
- Кеш операционной системы. Второе чтение того же файла быстрее первого — данные уже в кеше ОС. Замер чтения с диска после прогрева измеряет не диск, а память.
- Сборщик мусора. В
timeitGC по умолчанию отключён на время замера. В ручных замерах черезperf_counterон может сработать в середине и добавить время. Для чистоты можно временно отключить его черезgc.disable(), но не забудьте включить обратно. - Слишком короткий замер. Если операция быстрее разрешения таймера, один
perf_counterпокажет мусор или ноль. Это сигнал переходить наtimeit. - Замер на нагруженной машине. Если параллельно крутится сборка или тесты, минимум из
timeit.repeatчастично спасает, но лучше мерить на спокойной системе.
Что делать дальше
Замените в проекте все time.time() для интервалов на perf_counter() — это правка на минуту, которая убирает целый класс ложных замеров. Для сравнения вариантов алгоритма заведите привычку к timeit.repeat с минимумом вместо ручных однократных прогонов. Когда встанет вопрос «а где вообще тормозит», начинайте с cProfile и его вывода cumtime — ручной замер отдельных строк без профиля почти всегда бьёт мимо. Точные сигнатуры функций смотрите в документации модуля time и timeit для вашей версии Python.
1 Настенное время (wall-clock time) — реальное время, прошедшее по часам на стене, включая ожидание сети и диска. Противопоставляется процессорному времени, которое учитывает только работу CPU.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Python docs — time module (perf_counter, process_time), Python docs — timeit module
Частые вопросы
У меня Python 3.6, там есть perf_counter_ns?
Почему timeit возвращает большое число, хотя код быстрый?
Мой замер каждый раз разный, это нормально?
Можно ли мерить запрос к базе или API через timeit?
perf_counter показывает больше, чем ожидаю, — виноват мой код?
Как найти, какая именно строка функции тормозит?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.