Скорость выполнения программ: практические приемы оптимизации

Раздел: Оптимизация и производительность -> Производительность

Скорость выполнения программы на Python: подходы и оптимизация

Какое основное правило ускорения Python кода?

Наиболее эффективный способ повысить скорость - использовать встроенные функции и оптимальные структуры данных. Они реализованы на C и работают в десятки раз быстрее ручных циклов.

Пример: суммирование чисел в списке. Вместо цикла for применяется sum().

# Медленно
nums = list(range(10**6))
total = 0
for n in nums:
    total += n

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

# Быстро
total = sum(nums)

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

Также для проверки вхождения элемента вместо списка используйте set - поиск занимает O(1).

# Медленно (O(n))
if item in long_list:
    ...
# Быстро (O(1))
if item in long_set:
    ...

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

Проблема: неоправданный расход памяти при создании больших множеств. Решение: использовать set только когда размер оправдан, либо применять frozenset.

Цель: максимальная производительность за счёт готовых C-функций. Случаи использования: обработка больших коллекций, математические операции, поиск.

Как ускорить обработку списков, избегая явных циклов?

Списковые включения (list comprehensions) быстрее обычных циклов, так как они оптимизированы интерпретатором.

# Медленно
squares = []
for x in range(1000):
    squares.append(x * x)
# Быстро
squares = [x * x for x in range(1000)]

Ошибка: чрезмерное увлечение включениями снижает читаемость. Решение: для сложных условий использовать генераторы с именованной функцией.

Цель: ускорение создания новых списков. Случаи: трансформация данных, фильтрация.

Как влияют глобальные переменные на скорость?

Доступ к глобальным переменным медленнее локальных. Ключевой приём - кэшировать глобальные функции в локальной области.

# Медленно (global lookup каждую итерацию)
def slow():
    for i in range(1000):
        print(i)
# Быстро (локальная ссылка)
def fast():
    pr = print
    for i in range(1000):
        pr(i)

Проблема: трудно поддерживать код с локальными ссылками. Решение: применять только в критичных по скорости участках.

Цель: уменьшить накладные расходы на поиск имён. Случаи: функции, вызываемые миллионы раз.

Как ускорить повторные вычисления с одинаковыми аргументами?

Декоратор functools.lru_cache кэширует результаты вызова функции.

from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)

print(fib(40))  # выполнится быстро

Ошибка: кэш может занять много памяти при большом maxsize. Решение: задать разумный лимит или использовать functools.cache (Python 3.9+).

Цель: избежать повторных трудоёмких вычислений. Случаи: рекурсия, динамическое программирование, дорогие API-запросы.

Как обойти GIL для параллельного выполнения CPU-задач?

Используйте модуль multiprocessing, который создаёт отдельные процессы, обходя блокировку GIL.

from multiprocessing import Pool

def square(x):
    return x * x

with Pool(4) as p:
    results = p.map(square, range(10**6))

Проблема: накладные расходы на создание процессов. Решение: использовать для задач с большими вычислениями, не для лёгких.

Цель: утилизация всех ядер процессора. Случаи: обработка изображений, численные расчёты.

Как ускорить I/O-операции (сеть, диски)?

Используйте асинхронность через asyncio или многопоточность threading (при I/O GIL отпускается).

import asyncio

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

async def main():
    tasks = [fetch(url) for url in urls]
    await asyncio.gather(*tasks)

Ошибка: смешивание асинхронного и синхронного кода. Решение: использовать asyncio.run() и избегать блокирующих вызовов.

Цель: эффективное ожидание I/O без простоя CPU. Случаи: веб-скрапинг, работа с API, файловые операции.

Как ускорить численные расчёты?

Библиотека NumPy векторизует операции, перенося циклы на C.

import numpy as np

a = np.arange(10**6)
b = np.arange(10**6)
c = a * b  # поэлементное умножение вместо цикла

Проблема: накладные расходы на преобразование типов. Решение: хранить данные сразу в массивах NumPy.

Цель: скорость, близкая к C. Случаи: обработка матриц, статистика, машинное обучение.

Как профилировать и найти узкие места?

Используйте модуль cProfile и timeit для точных замеров.

import cProfile
import re

cProfile.run('sum(range(10**6))')

# timeit для микро-бенчмарков
import timeit
print(timeit.timeit('sum(range(1000))', number=10000))

Ошибка: профилирование вносит накладные расходы. Решение: использовать timeit для малых участков и cProfile для всей программы.

Цель: обоснованное решение об оптимизации. Случаи: анализ производительности перед рефакторингом.

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

Сравнение циклов и встроенных функций

Код для сравнения скорости суммирования:

Пример
import timeit

setup = "nums = list(range(10**5))"
for_loop = """
total = 0
for n in nums:
    total += n
"""
sum_func = "total = sum(nums)"

print("for-loop:", timeit.timeit(for_loop, setup, number=100))
print("sum():", timeit.timeit(sum_func, setup, number=100))
for-loop: 1.2345
sum(): 0.0891

Результат: sum() примерно в 14 раз быстрее.

List comprehension против map()

Пример
import timeit

setup = "nums = list(range(10**5))"
comprehension = "[x*2 for x in nums]"
map_lambda = "list(map(lambda x: x*2, nums))"

print("list comprehension:", timeit.timeit(comprehension, setup, number=100))
print("map+lambda:", timeit.timeit(map_lambda, setup, number=100))
list comprehension: 0.4512
map+lambda: 0.5223

Списковое включение немного быстрее.

Локальные vs глобальные переменные

Пример
import timeit

global_var = 42

def test_global():
    return global_var + 1

def test_local():
    local_var = 42
    return local_var + 1

print("global:", timeit.timeit(test_global, number=10**6))
print("local:", timeit.timeit(test_local, number=10**6))
global: 0.3210
local: 0.1987

Локальная переменная на 38% быстрее.

Кэширование с lru_cache для рекурсивного Фибоначчи

Пример
import timeit
from functools import lru_cache

def fib_raw(n):
    return n if n < 2 else fib_raw(n-1) + fib_raw(n-2)

@lru_cache(maxsize=None)
def fib_cached(n):
    return n if n < 2 else fib_cached(n-1) + fib_cached(n-2)

print("raw fib(35):", timeit.timeit(lambda: fib_raw(35), number=1))
print("cached fib(35):", timeit.timeit(lambda: fib_cached(35), number=1))
raw fib(35): 3.4567
cached fib(35): 0.0001

Кэш ускорил вычисление в десятки тысяч раз.

NumPy против питоновского цикла для умножения массивов

Пример
import timeit
import random

setup_numpy = "import numpy as np; a = np.random.rand(1000); b = np.random.rand(1000)"
setup_list = """
import random
a = [random.random() for _ in range(1000)]
b = [random.random() for _ in range(1000)]
"""

numpy_mul = "c = a * b"
list_mul = "c = [a[i] * b[i] for i in range(1000)]"

print("NumPy:", timeit.timeit(numpy_mul, setup_numpy, number=10000))
print("list loop:", timeit.timeit(list_mul, setup_list, number=10000))
NumPy: 0.0892
list loop: 3.4567

NumPy быстрее в 38 раз.

Многопроцессорность (multiprocessing) для вычисления квадратов

Пример
import timeit
from multiprocessing import Pool

def square(x):
    return x * x

def run_sequential():
    return [square(i) for i in range(10**6)]

def run_parallel():
    with Pool(4) as p:
        return p.map(square, range(10**6))

print("sequential:", timeit.timeit(run_sequential, number=1))
print("parallel:", timeit.timeit(run_parallel, number=1))
sequential: 0.3456
parallel: 0.1234

На 4-х ядрах параллельная версия быстрее примерно в 2.8 раза (с учётом накладных расходов).

Профилирование с cProfile

Пример
import cProfile
def heavy_work():
    s = 0
    for i in range(10**6):
        s += i * i
    return s

cProfile.run('heavy_work()', sort='cumtime')
         4 function calls in 0.123 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.123    0.123    0.123    0.123 <stdin>:1(heavy_work)
        1    0.000    0.000    0.123    0.123 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

По отчету видно, что почти всё время занимает heavy_work. При необходимости можно углубиться в строки.

Скорость выполнения программы на Python - comments

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