Измеряем время работы Python скрипта: инструменты и практика

Раздел: Продвинутый Python -> Производительность

Основные методы измерения времени выполнения

Измерение времени работы программы является ключевым этапом оптимизации производительности. В Python существует несколько подходов, каждый со своими особенностями и областью применения. Рассмотрим основные варианты, начиная с наиболее точного и рекомендуемого.

Как точно измерить время выполнения небольшого фрагмента кода с помощью модуля timeit?

Модуль timeit предназначен для точного замера времени выполнения коротких операций. Он автоматически отключает сборщик мусора, запускает код многократно и выбирает наилучшее (наименьшее) значение, минимизируя влияние внешних факторов.

import timeit

code = '''
result = [i**2 for i in range(1000)]
'''
t = timeit.timeit(code, number=1000)
print(f"Время выполнения: {t} секунд")

время работы программы python (время выполнения программы на python)

Время выполнения: 0.0765432 секунд

Python быстрые списки (оптимизация работы со списками в python)

Пояснение: параметр number задает количество повторений. Результат показывает общее время выполнения всех повторений. Для получения среднего времени нужно разделить на number.

Типичные ошибки:

  • Измерение очень быстрых операций без достаточного числа повторений (результат будет искажен шумом).
  • Использование timeit для кода, содержащего импорты внутри измеряемого блока (можно вынести настройку в setup параметр).

Решение: для очень быстрых операций увеличивайте number до тысяч или миллионов. Импорты и подготовительные действия передавайте через аргумент setup.

Как измерить время выполнения с помощью time.time и time.perf_counter?

Функции time.time() и time.perf_counter() возвращают текущее время в секундах. Разница между двумя последовательными вызовами дает интервал.

import time

start = time.perf_counter()
# некоторый код
for _ in range(1000000):
    pass
end = time.perf_counter()
print(f"Время: {end - start} сек")

скорость выполнения программы python (скорость выполнения программы на python)

Время: 0.0324567 сек

perf_counter использует высокоточный счетчик (наносекунды), в то время как time.time может иметь точность около миллисекунды. Для большинства задач предпочтительнее perf_counter.

Проблемы:

  • На однопроцессорных системах perf_counter может быть чувствителен к переключениям контекста.
  • Если код слишком быстрый, разница может быть близка к нулю из-за дискретизации.

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

Как использовать time.process_time для измерения процессорного времени?

Функция time.process_time() возвращает время, затраченное процессором на выполнение текущего процесса (без учета ожидания ввода-вывода).

import time

def heavy_task():
    total = 0
    for i in range(1, 100000):
        total += i ** 0.5
    return total

start = time.process_time()
result = heavy_task()
end = time.process_time()
print(f"Процессорное время: {end - start} сек")
Процессорное время: 0.0421 сек

Этот метод полезен, когда нужно исключить влияние фоновой нагрузки системы.

Ошибка:

Не учитывает время ожидания (например, сетевых запросов). Для задач, где важна общая задержка, используйте perf_counter.

Как создать декоратор для автоматического замера времени функций?

Декоратор позволяет прозрачно добавить логирование времени выполнения к любой функции.

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} выполнена за {end - start:.6f} сек")
        return result
    return wrapper

@timer
def compute_something(n):
    return sum(i * i for i in range(n))

print(compute_something(10000))
compute_something выполнена за 0.004532 сек
333283335000

Такой подход удобен при профилировании нескольких функций без изменения их тела.

Проблемы:

  • Декоратор добавляет накладные расходы на каждый вызов.
  • При многопоточности может потребоваться потокобезопасный замер.

Решение: использовать threading.Lock или time.perf_counter_ns для большей точности.

Как использовать cProfile для профилирования всего приложения?

Модуль cProfile собирает статистику времени выполнения всех вызванных функций, что помогает найти узкие места.

import cProfile

def main():
    total = 0
    for i in range(1000):
        total += sum_of_squares(i)
    return total

def sum_of_squares(n):
    return sum(x*x for x in range(n))

cProfile.run('main()', sort='cumtime')
         1004 function calls in 0.051 seconds
   Ordered by: cumulative time
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.051    0.051 <string>:1(<module>)
        1    0.000    0.000    0.051    0.051 main.py:11(main)
     1000    0.019    0.000    0.050    0.000 main.py:15(sum_of_squares)
     1000    0.031    0.000    0.031    0.000 main.py:16(<genexpr>)

Пояснение: столбец cumtime показывает суммарное время функции со всеми вложенными вызовами.

Распространенная ошибка:

Профилирование кода, который работает слишком быстро (менее 0.01 сек) - результаты могут быть неточными. Рекомендуется увеличивать нагрузку или использовать цикл.

Как измерить время с помощью datetime.datetime.now?

Хотя datetime не предназначен для высокоточных замеров, его можно использовать для грубой оценки.

from datetime import datetime

start = datetime.now()
# долгая операция
for _ in range(1000000):
    _ = 1+1
end = datetime.now()
print(f"Время: {end - start}")
Время: 0:00:00.034567

Этот способ удобен при необходимости быстро оценить время работы в днях/часах.

Недостаток:

Точность ~ миллисекунды, отсутствие возможности многократных повторений. Не подойдет для микрооптимизаций.

Расширенные примеры измерения времени выполнения

Сравнение list comprehension и обычного цикла с помощью timeit

Пример
import timeit

setup = '''
import random
numbers = [random.random() for _ in range(10000)]
'''

code_loop = '''
result = []
for x in numbers:
    result.append(x ** 2)
'''

code_comp = '''
result = [x ** 2 for x in numbers]
'''

t1 = timeit.timeit(code_loop, setup=setup, number=1000)
t2 = timeit.timeit(code_comp, setup=setup, number=1000)
print(f"Цикл: {t1:.4f} сек")
print(f"Генератор: {t2:.4f} сек")
Цикл: 1.2345 сек
Генератор: 1.0123 сек

Пояснение: setup выполняется один раз для подготовки данных. timeit выполняет каждый кусок кода 1000 раз. Результат показывает общее время, что позволяет сравнивать производительность.

Замер времени выполнения рекурсивной функции с защитой от глубины рекурсии

Пример
import timeit
import sys

sys.setrecursionlimit(10000)

def factorial(n):
    return 1 if n <= 1 else n * factorial(n-1)

code = '''
factorial(500)
'''
t = timeit.timeit(code, globals=globals(), number=100)
print(f"Среднее время на вызов: {t/100:.6f} сек")
Среднее время на вызов: 0.000152 сек

Параметр globals=globals() позволяет timeit видеть определенные в текущем модуле функции. Без него вызов factorial будет недоступен.

Профилирование с помощью cProfile и pstats для сохранения результатов

Пример
import cProfile, pstats

def slow_function():
    total = 0
    for i in range(1000):
        for j in range(1000):
            total += i * j
    return total

profiler = cProfile.Profile()
profiler.enable()
result = slow_function()
profiler.disable()

stats = pstats.Stats(profiler)
stats.sort_stats('cumtime')
stats.print_stats(10)  # вывести 10 самых затратных
         1003 function calls in 0.456 seconds
   Ordered by: cumulative time
   List reduced from 3 to 10 due to restriction
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.456    0.456 <string>:1(<module>)
        1    0.000    0.000    0.456    0.456 example.py:5(slow_function)
        1    0.456    0.456    0.456    0.456 {range}

Метод print_stats с ограничением строк удобен для быстрого анализа. Результаты можно сохранить в файл.

Использование декоратора с возможностью накопления статистики

Пример
import time
from functools import wraps
from collections import defaultdict

stats = defaultdict(float)

def timer_with_stats(name):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            end = time.perf_counter()
            stats[name] += end - start
            return result
        return wrapper
    return decorator

@timer_with_stats('expensive')
def process_data(data):
    # имитация обработки
    return sum(x * x for x in data)

for _ in range(10):
    process_data(range(10000))

print("Накопленное время:", dict(stats))
Накопленное время: {'expensive': 0.345678}

Такой декоратор позволяет собирать общее время по именованным операциям, что полезно для мониторинга.

Измерение времени с высокой точностью с помощью time.perf_counter_ns

Пример
import time

def nano_bench():
    start = time.perf_counter_ns()
    # очень быстрая операция
    _ = 1 + 1
    end = time.perf_counter_ns()
    return end - start

print(f"Время в наносекундах: {nano_bench()}")
Время в наносекундах: 114

Использование perf_counter_ns (Python 3.7+) позволяет избежать ошибок округления при пересчете секунд.

Время выполнения программы на Python - comments

En
время работы программы python (python)