Numba: компилятор для numpy-кода
- О чём эта тема
- Декоратор @njit, который превращает медленный питоновский цикл в машинный код: как это работает, что ускоряется в десятки раз, что не ускоряется вовсе — и как честно измерить разницу.
- Аннотация
- Линия P начинается с инструмента, на котором она построена. Numba — JIT-компилятор: в момент первого вызова функции он выводит типы аргументов, транслирует байткод Python в промежуточное представление LLVM и получает машинный код, неотличимый по скорости от C. Конспект показывает это на живом замере: цикл Монте-Карло, оценивающий π, ускоряется в 59 раз одной строчкой-декоратором. Разбирается режим nopython и ошибка TypingError, цена первого вызова (компиляция) и правило честного замера, а главное — граница применимости: числа, циклы и numpy-массивы Numba любит, а словари, pandas и строки — нет. Тренажёр тренирует чутьё: по фрагменту кода предсказать, ускорит его @njit или нет. Все примеры и числа конспекта проверены запуском.
- Пререквизиты
- Python и numpy на базовом уровне. Знакомство с линией C не требуется — до P04 видеокарта не понадобится вовсе.
- Мотивация
- «Python медленный» — полуправда: медленный интерпретатор, а не машина под ним. Обычный выход — переписать горячее место на C++ — стоит дней работы. Numba предлагает сделку получше: та же функция, тот же Python, плюс одна строка — и внутри уже работает настоящий компилятор. Дальше в линии на этом фундаменте вырастут многопоточность (P02) и ядра для видеокарты (P04); но сначала нужно понять, что именно делает волшебная строка и где кончается волшебство.
1. Одна строка — ускорение в 59 раз
Возьмём честную вычислительную задачу: метод Монте-Карло для π. Бросаем n случайных точек в единичный квадрат и считаем, какая доля попала в четверть круга; доля, умноженная на 4, стремится к π. Чистый цикл, ничего кроме арифметики:
import numpy as np from numba import njit def monte_carlo_pi(n): inside = 0 x = 1234567.0 for i in range(n): # простенький генератор случайных чисел прямо в цикле, # чтобы сравнение python/numba было работой один в один x = (1103515245 * x + 12345) % 2147483648 px = x / 2147483648 x = (1103515245 * x + 12345) % 2147483648 py = x / 2147483648 if px * px + py * py <= 1.0: inside += 1 return 4.0 * inside / n fast_pi = njit(monte_carlo_pi) # та же функция + компилятор; обычно пишут @njit
Запускаем обе версии на двух миллионах точек (замеры этого конспекта — реальные, с обычного настольного процессора):
| Вызов | Время | Комментарий |
|---|---|---|
| monte_carlo_pi(2_000_000) | 0.76 с | чистый Python: каждый шаг цикла — работа интерпретатора |
| fast_pi — первый вызов | 1.60 с | медленнее Python! Внутри — компиляция (см. раздел 2) |
| fast_pi — второй вызов | 12.9 мс | в 59 раз быстрее Python; результат тот же: 3.1675 |
Никакой параллельности здесь ещё нет — одно ядро, один поток. Всё ускорение — из отказа от интерпретатора: вместо «прочитать байткод, определить тип x, найти метод сложения, упаковать результат в объект» на каждой строчке — голые машинные инструкции над числами в регистрах, как в C.
2. Что происходит при первом вызове
Numba — компилятор just-in-time: он работает не заранее, а в момент вызова, когда уже известны настоящие типы аргументов. Конвейер такой:
функция как есть
x: float64, n: int64…
под ваш CPU
Ключевой этап — вывод типов: по типам аргументов первого вызова Numba прослеживает каждый оператор функции и приписывает каждой переменной конкретный машинный тип. Скомпилированный результат кэшируется на пару «функция + сигнатура типов»: вызов fast_pi с int64 использует готовый код, а вызов с float64 запустит новую компиляцию. Флаг @njit(cache=True) сохраняет результат на диск — и следующий запуск программы обойдётся без повторной компиляции.
Приставка «n» в njit означает режим nopython: компилируется всё или ничего. Если в функции встретилось что-то, что Numba не умеет превратить в машинный код, вы получите не тихое замедление, а честную ошибку компиляции:
@njit def bad(d): return d["a"] bad({"a": 1}) # TypingError: Failed in nopython mode pipeline (step: nopython frontend) # ...обычный словарь Python компилятору не по зубам
Это осознанный дизайн: старший декоратор @jit умел «object mode» — прозрачно откатываться к интерпретатору на непонятных местах, — и программы годами работали медленно, не сообщая об этом. TypingError неприятен ровно один раз, зато после него вы точно знаете: раз скомпилировалось — значит, быстро.
3. Граница волшебства: что ускоряется, а что нет
Numba создана для числового кода и не претендует на весь Python. Практическая карта:
| Код | @njit | Почему |
|---|---|---|
| циклы с арифметикой | да, в десятки раз | главный клиент: интерпретатор больше не трогает каждую итерацию |
| numpy-массивы и большинство np-функций | да | Numba знает их типы и внутреннее устройство; поддержан большой кусок numpy API |
| math.*, кортежи, свои @njit-функции | да | всё это типизируется; njit-функции зовут друг друга без накладных расходов |
| однократный вызов np.dot больших матриц | ускорения не будет | np.dot и так выполняется библиотекой BLAS на C — интерпретатор там не узкое место |
| словари/списки объектов, свои классы | нет (TypingError) | динамические структуры не типизируются; для классов есть отдельный @jitclass |
| pandas, строки, файлы, сеть | нет | не числовой код; pandas-объекты Numba не знает вовсе |
Отсюда рабочий приём: не заворачивайте в @njit программу целиком — выносите горячее вычислительное ядро в отдельную функцию с числами и массивами на входе-выходе, и компилируйте её. Загрузка данных, pandas-обвязка и графики остаются обычным Python, которому и так хорошо.
4. Тренажёр: ускорит или нет
Шесть фрагментов кода. Для каждого решите: заметно ли его ускорит @njit — или декоратор бесполезен либо вовсе не скомпилируется.
Контрольные вопросы
-
В первый вызов входит вся компиляция: вывод типов, генерация LLVM-кода, машинный код. Это разовая плата; дальше работает готовый код. Для замеров — прогревочный вызов вне секундомера; для продакшена — @njit(cache=True), сохраняющий скомпилированное на диск между запусками программы.
-
Компилируется всё или ничего: встретив нетипизируемую конструкцию, Numba бросает TypingError вместо того, чтобы молча выполнить кусок через интерпретатор. Ошибка при разработке лучше незаметной медленности в проде: раз скомпилировалось — значит, действительно быстро.
-
Из отказа от интерпретатора: в чистом Python каждая итерация — это чтение байткода, динамическое определение типов, поиск методов и упаковка чисел в объекты. Скомпилированный код выполняет голые машинные инструкции над числами в регистрах — как программа на C, на том же одном ядре.
-
np.dot и так выполняется скомпилированной библиотекой BLAS; интерпретатору принадлежат микросекунды на диспетчеризацию вызова. Numba ускоряет пользовательские циклы, где интерпретатор работает на каждой итерации, — а не внутренности чужих библиотек.
-
Типы выводятся из фактических аргументов первого вызова; результат кэшируется на сигнатуру. Вызов с другой комбинацией типов запускает отдельную компиляцию — у одной функции может быть несколько машинных версий под разные сигнатуры.
Источники
- A ~5 minute guide to Numba // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/user/5minguide.html (дата обращения: 09.07.2026).
- Compiling Python code with @jit // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/user/jit.html (дата обращения: 09.07.2026).
- Supported NumPy features // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/reference/numpysupported.html (дата обращения: 09.07.2026).