Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Cython и Numba: ускорение Python-кода

Cython и Numba: когда и как ускорять Python-код в десятки раз

Практика ускорения вычислений: лестница оптимизации от алгоритма к numba/cython, JIT-компиляция numba (njit, кеш, ограничения), статическая компиляция Cython, сравнение и критерии выбора.

Прямой ответ

Cython и Numba решают одну задачу — выполняют «горячий» Python-код со скоростью C — разными путями: Numba — JIT-компиляция функций в машинный код LLVM (декоратор @njit над обычной Python-функцией, нулевая сборка, идеален для численных циклов над numpy-массивами), Cython — статическая компиляция расширенного Python-синтаксиса (.pyx с C-типами) в C-расширение (сборка при установке, максимум контроля, интеграция с C-библиотеками). Но оба — последние ступени лестницы: сначала алгоритм и структура данных, потом векторизация numpy (часто даёт те же 10–100× без компиляторов вообще — основы), и только не выражаемый векторно CPU-цикл заслуживает JIT/компиляции. Без профиля (инструменты) оба инструмента — стрельба по площадям.

Numba: JIT без сборки

Минимальный кейс: @numba.njit (nopython-режим) на функции с циклом по массивам — первый вызов компилирует (сотни мс), последующие исполняют машинный код (ускорение 10–200× на численных петлях типично). Что компилируется хорошо: арифметика над numpy-массивами и скалярами, циклы, простые структуры (кортежи, типизированные списки); что плохо/никак: произвольные Python-объекты, pandas-объекты (сначала .to_numpy()), большая часть стандартной библиотеки (поддерживаемый набор — документация numba). Практические приёмы: cache=True (скомпилированное на диск — холодный старт не компилирует заново), parallel=True + prange (распараллеливание цикла по ядрам — многопоточность без GIL-ограничений для скомпилированного кода), явные сигнатуры для специализации. Ловушка «компиляция съела выигрыш» — на коротких вызовах: JIT окупается на повторяющихся вызовах с большими данными, не на микроскопических.

Cython: статическая компиляция и C-интеграция

Cython-модуль — .pyx-файл: Python-синтаксис + C-декларации (cdef int i, cdef double[:] arr — типизированные memoryview), компиляция через C-компилятор в расширение (сборка описывается в pyproject/setup — часть процесса упаковки). Ускорение требует дисциплины типов: нетипизированный .pyx почти не быстрее Python; типизированные циклы — уровень C. Отдельная суперсила — вызов C/C++-библиотек напрямую (cdef extern from "math.h") — обёртки к нативным библиотекам пишутся на Cython, а не на ctypes, когда важна скорость границы. Цена: тулчейн сборки (C-компилятор у разработчиков и в CI), отладка сложнее (код на стыке языков), порог входа выше numba. Для «просто ускорить цикл» в 2026-м numba почти всегда разумнее; Cython — для библиотечного кода и C-интеграций.

Лестница оптимизации: где эти инструменты стоят

Честный порядок действий при медленном CPU-коде: (1) профиль — что именно горячее (часто — не то, что казалось); (2) алгоритм/структуры (O(n²)→O(n log n), dict вместо линейного поиска — даёт порядки, которых компиляторы не дадут); (3) векторизация numpy (цикл → операция над массивом; даже «невозможные» случаи часто выражаются через where/cumsum/broadcasting); (4) специализированные движки (для табличного — DuckDB/pandas-агрегации; для графов/поиска — готовые библиотеки); (5) numba — цикл остался, он численный и горячий; (6) cython/C — numba не справляется (сложные структуры, C-интеграция) или нужен максимум. Пропуск первых трёх ступеней — типичная ошибка: «обернул в njit код, который векторизуется одной строкой» — сложность навсегда, выигрыш тот же.

Сравнение по критериям

Порог входа: numba — декоратор; cython — синтаксис+сборка. Дистрибуция: numba — чистый Python-пакет (зависимость numba+llvmlite); cython — колёса под платформы (CI-сборка, матрица). Производительность пика: cython чуть выше на сложных кейсах (полный контроль, нет runtime-проверок JIT); numba близок на численных циклах. Параллелизм: у обоих есть (numba parallel/prange; cython — openmp/nogil-блоки); до них — честная оценка, не упирается ли задача в память/алгоритм (многопроцессность и GIL — отдельная тема). Стабильность экосистемы: оба зрелые; numba активнее развивается вокруг scientific-стека. Для 90% прикладных задач «ускорить вычисление» правильный ответ в 2026: numpy-векторизация, затем numba.

Практика внедрения numba (рецепт)

Типовой путь без сюрпризов: выделить горячую функцию (профиль) → переписать её «numba-совместимо» (входы/выходы — numpy-массивы и скаляры, без Python-объектов внутри) → @njit(cache=True) → прогнать тесты (результат должен совпадать до машинной точности — юнит-тест «медленная версия против быстрой» на случайных данных) → замерить wall-time на реальных объёмах. Разделение слоёв: Python-код оркестрирует (I/O, pandas, логика), numba-функции — чистые вычислительные ядра (легко тестировать и заменять). Ошибки компиляции numba читаемы (указывает неподдерживаемую конструкцию) — лечатся переформулировкой, а не борьбой. И фиксация результата: «было 40 с, стало 1.2 с» — в комментарий/задачу (оптимизации без цифр откатываются при первом рефакторинге).

Практический сценарий: ускорение расчёта скользящего окна

Возьмём характерную задачу: для каждого элемента массива из двух миллионов значений посчитать взвешенное среднее по окну в пятьдесят соседей. Первая версия «в лоб» — двойной цикл на чистом Python — идёт минуты. Вторая — векторизация через развёртку окна в двумерный массив и умножение на вектор весов — падает по памяти (2M × 50 float64 — восемьсот мегабайт копий) либо делается через накопительные суммы, если веса позволяют. Третья версия — та же двойная петля, но с @njit(cache=True): numba компилирует её в машинный код, и расчёт занимает секунды — с сохранением исходной логики строчка в строчку. Именно этот паттерн «векторизация не выразима без копий — обернули плотный числовой цикл в njit» — основная ниша numba в прикладном коде. Проверка корректности обязательна: на случайной выборке из тысячи элементов результат njit-версии сравнивается с эталонной медленной (совпадение до машинной точности), и этот тест живёт в репозитории — он же поймает расхождение при обновлении numba. Замер — на реальных объёмах, «до/после» фиксируется в задаче: было девяносто секунд, стало четыре. Если позже профиль покажет, что узкое место переехало в чтение данных — цикл трогать больше не нужно, оптимизация закончена.

FAQ

Numba замедляет первый вызов — это проблема? Для долгоживущих процессов/пакетных задач — нет (компиляция один раз, cache=True помогает между запусками). Для лямбда-вызовов/коротких скриптов — да, там JIT-подогрев не окупается; альтернатива — предкомпиляция AOT или cython-колесо.

PyPy вместо numba/cython? PyPy ускоряет «обычный Python» целиком (свой JIT), но живёт своей экосистемой (C-расширения — через совместимость с оговорками, numpy медленнее). Для «ускорить численный код в CPython-проекте» numba практичнее; PyPy — для проектов без тяжёлых C-зависимостей.

Можно ли numba с pandas? Напрямую — ограниченно (объекты pandas не поддерживаются); паттерн — вынуть массивы (.to_numpy()), посчитать в njit-функции, вернуть результат обратно в DataFrame. Часто после векторизации pandas надобность в numba отпадает вовсе.

Что выбрать для обучения/прототипа? numpy + numba: оба — Python-уровень, итерации быстрые, понимание переносится. Cython учите, когда задача действительно про C-интеграцию или дистрибуцию производительности как библиотеки — это уже инженерная специализация.

Источники: документация Numba и Cython, справочник NumPy — ссылки в шапке.

FAQ

Можно ли сочетать numba и многопроцессность? Да, и они ортогональны: numba ускоряет вычисление внутри процесса (включая свой parallel/prange по потокам без GIL), multiprocessing размножает процессы. Практичнее начать с numba — если задача уперлась в одно ядро, — и только потом добавлять процессы, если уперлась в память или нужна переживаемость сбоев.

FAQ

Можно ли сочетать numba и многопроцессность? Да, они ортогональны: numba ускоряет вычисление внутри процесса (включая свой parallel/prange по потокам без GIL), multiprocessing размножает процессы. Практичнее начать с numba — если задача уперлась в одно ядро, — и только потом добавлять процессы, если уперлась в память или нужна переживаемость сбоев.