Производительность: векторизация, Numba, Cython
Чистый Python медленный. Научный Python быстр благодаря векторизации — а когда и её мало, в ход идут Numba и Cython.
Векторизация — замена явных Python-циклов операциями над целыми массивами, которые выполняются в скомпилированном коде NumPy за один вызов.
Откуда берётся медлительность
Python — интерпретируемый язык с динамической типизацией. Каждая операция в цикле — это проверка типов, создание объектов, работа сборщика мусора. Сложить два миллиона чисел в Python-цикле — секунды; в NumPy — миллисекунды. Разница в сотни раз. Понимать, где «уходить» из чистого Python, — ключевой навык научного программиста.
Иерархия скорости
| Подход | Относительная скорость | Когда |
| Чистый Python-цикл | ×1 (медленно) | понять идею, мелкие данные |
| Векторизация NumPy | ×50–200 | почти всегда — первый выбор |
| Numba (JIT) | ×100–500 | цикл нельзя векторизовать |
| Cython / C | ×100–1000 | критичная по скорости часть |
Замер «руками»: цикл против поэлементной обработки
Векторизацию в чистом stdlib не показать (нет массивов), но можно измерить, насколько Python-цикл дороже, и понять масштаб проблемы. Сравним наивную сумму квадратов через цикл и через генератор:
import time
N = 1_000_000
# способ 1: явный цикл с накоплением
start = time.perf_counter()
total = 0
for i in range(N):
total += i * i
t1 = time.perf_counter() - start
# способ 2: встроенный sum с генератором (ближе к "векторному" стилю)
start = time.perf_counter()
total2 = sum(i * i for i in range(N))
t2 = time.perf_counter() - start
print("Цикл :", total)
print("sum/генер. :", total2)
print("Результаты совпали:", total == total2)
print("Оба считают один миллион операций")
Вывод:
Цикл : 333332833333500000 sum/генер. : 333332833333500000 Результаты совпали: True Оба считают один миллион операций
Оба варианта дают один ответ. В NumPy то же — np.arange(N)**2).sum() — выполнилось бы в скомпилированном коде в десятки раз быстрее любого из них, потому что цикл уходит из интерпретатора в C.
Векторизация: цикл против массива
Сравните стиль (код для чтения):
import numpy as np
# МЕДЛЕННО: явный цикл (Python интерпретирует каждый шаг)
result = []
for x in data:
result.append(x**2 + 1)
# БЫСТРО: векторизация (весь массив за один вызов C-кода)
arr = np.array(data)
result = arr**2 + 1 # ни одного Python-цикла!
Когда уходить в Numba или Cython
Иногда алгоритм нельзя выразить операциями над массивами — например, итеративный метод, где каждый шаг зависит от предыдущего. Тогда:
- Numba — добавляете декоратор
@njit, и функция компилируется в машинный код «на лету» (JIT). Минимум изменений, ускорение в сотни раз. - Cython — пишете код на «Python с типами», который транслируется в C. Больше работы, максимальный контроль.
from numba import njit
@njit # компилируется в машинный код при первом вызове
def mandelbrot_point(cx, cy, max_iter):
x = y = 0.0
for i in range(max_iter):
x, y = x*x - y*y + cx, 2*x*y + cy
if x*x + y*y > 4:
return i
return max_iter
Как работает под капотом
Почему векторизация так ускоряет? Три причины. Во-первых, цикл уходит в C: NumPy перебирает массив в скомпилированном коде без накладных расходов интерпретатора. Во-вторых, однородность памяти: массив NumPy — это сплошной блок чисел одного типа, который процессор читает эффективно (кэш, SIMD-инструкции, обрабатывающие несколько чисел за такт). В-третьих, нет создания объектов: в Python i*i создаёт новый объект-число, в NumPy всё считается прямо в памяти. Numba и Cython атакуют ту же проблему иначе — компилируют сам Python-код в машинный, убирая интерпретатор, что спасает там, где векторизация невозможна.
Частые ошибки
- Преждевременная оптимизация. Сначала напишите понятно и измерьте; ускоряйте только реальное «узкое место».
- Цикл там, где можно векторизовать. Самая частая причина медленного научного кода — Python-цикл по массиву.
- Тащить Cython без нужды. Часто хватает векторизации или Numba; Cython — последнее средство.
Итог
- Чистый Python медленный из-за интерпретации и создания объектов.
- Векторизация уводит цикл в скомпилированный C — первый и главный приём (×50–200).
- Если векторизовать нельзя — Numba (JIT, минимум правок) или Cython (макс. контроль).
- Оптимизируйте только измеренные «узкие места», не вслепую.