Траектория «Параллельные вычисления» · линия P · конспект 1 из 6

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.

Типичная ошибка Замерить первый вызов и заключить, что «Numba медленнее Python» (1.6 с против 0.76 с — формально так и есть!). Первый вызов оплачивает компиляцию; сама функция после него летает. Правило честного замера: один прогревочный вызов вне секундомера, затем измерение — лучше среднее по нескольким повторам. Это в точности «прогрев» из конспекта C06 — накладные расходы первого раза есть и у видеокарт, и у компиляторов.

2. Что происходит при первом вызове

Numba — компилятор just-in-time: он работает не заранее, а в момент вызова, когда уже известны настоящие типы аргументов. Конвейер такой:

байткод Python
функция как есть
→ вывод типов →
типизированное IR
x: float64, n: int64…
→ LLVM →
машинный код
под ваш 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, которому и так хорошо.

Типичная ошибка Обвесить @njit код, который и так проводит всё время внутри numpy: например, функцию из трёх строк с np.dot и np.sum по гигантским массивам. Эти операции уже выполняются скомпилированными библиотеками; @njit в лучшем случае ничего не изменит. Правило: Numba ускоряет ваши циклы, а не чужие библиотеки. Если циклов нет — сначала спросите себя, где программа проводит время (профилировщик), потом тянитесь за декоратором.

4. Тренажёр: ускорит или нет

Тренажёр · предскажите эффект @njit

Шесть фрагментов кода. Для каждого решите: заметно ли его ускорит @njit — или декоратор бесполезен либо вовсе не скомпилируется.

Разбор не начат.

Контрольные вопросы

Источники

  1. A ~5 minute guide to Numba // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/user/5minguide.html (дата обращения: 09.07.2026).
  2. Compiling Python code with @jit // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/user/jit.html (дата обращения: 09.07.2026).
  3. Supported NumPy features // Numba Documentation : [сайт]. — URL: https://numba.readthedocs.io/en/latest/reference/numpysupported.html (дата обращения: 09.07.2026).