Блог им. MihailMihalev

Танцуем Numba

Пишешь код на python, а он тормозит и хочется взять и переписать на C++?
Не надо так! Современный С++ компилятор имеет под капотом LLVM, но современный python настолько мощный, что можно использовать тот же самый LLVM, чтобы получить скорость сопоставимую с C++. Вот типичный пример того, что на голом python будет работать мучительно долго: 

import time
import numba
import numpy as np

@numba.jit(nopython=True)
def calculate_max_drawdown(initial_cash: float, cashes: np.ndarray) -> float:
    """Расчет максимальной просадки"""
    peak = initial_cash
    max_dd = 0.0

    for cash in cashes:
        if cash > peak:
            peak = cash
        dd = (peak - cash) / peak
        if dd > max_dd:
            max_dd = dd

    return max_dd

# Вариант без Numba для сравнения
def calculate_max_drawdown_pure(initial_cash: float, cashes: np.ndarray) -> float:
    peak = initial_cash
    max_dd = 0.0

    for cash in cashes:
        if cash > peak:
            peak = cash
        dd = (peak - cash) / peak
        if dd > max_dd:
            max_dd = dd

    return max_dd

def test_calculate_max_drawdown():
    n = 1_000_000  # 1 миллион точек
    returns = np. <a name="cut"></a> random.randn(n) * 0.001  # случайные доходности ~0.1%
    cashes = 100000.0 * np.exp(np.cumsum(returns))  # геометрическое блуждание

    # Первый вызов для компиляции Numba (исключаем из замера)
    _ = calculate_max_drawdown(100000.0, cashes[:1000])

    # Тестирование версии с Numba
    start = time.perf_counter()
    dd_numba = calculate_max_drawdown(100000.0, cashes)
    numba_time = time.perf_counter() - start

    # Тестирование чистой Python версии
    start = time.perf_counter()
    dd_pure = calculate_max_drawdown_pure(100000.0, cashes)
    pure_time = time.perf_counter() - start

    print(f"Результаты теста на {n:,} точках:")
    print(f"Максимальная просадка: {dd_numba:.4%}")
    print(f"Время Numba: {numba_time:.4f} сек")
    print(f"Время чистый Python: {pure_time:.4f} сек")
    print(f"Ускорение: {pure_time/numba_time:.1f}x")
    print(f"Результаты совпадают: {abs(dd_numba - dd_pure) < 1e-10}")

if __name__ == "__main__":
    test_calculate_max_drawdown()


Вывод программы:

Танцуем Numba

При первом вызове функции с декоратором numba.jit  Numba скомпилирует функцию через LLVM в машинный код. Скорость сравнима с ручным C++. Ускорение — 100-200x.

Что под капотом: Numba берёт байт-код python функции, строит LLVM IR, применяет агрессивные оптимизации: инлайнинг, векторизацию SIMD инструкций, оптимизацию доступа к памяти. Всё это — тот же туллинг, что использует Clang (C++ компилятор на LLVM).

Где применять: любые плотные вычисления над массивами — кастомные индикаторы, статистические расчёты, симуляции, фильтрация данных.

Ограничения: внутри @numba.jit — только NumPy, примитивные типы, math.*, часть numpy.*. Нет pandas, нет произвольных Python-объектов.

Numba — это как писать на С++, только на питоне:)

260
#52 по плюсам, #40 по комментариям
6 комментариев
В принципе, иногда векторные операции можно задействовать, как в этом примере. Тоже быстро будет.
avatar
Riskplayer, Конкретно в этом примере цикл не параллелится, потому что будет гонка, но да, некоторые циклы можно параллелить. Если нужен массовый параллелизм, то лучше использовать taichi с бэкендом cuda или vulkan.
Михаил Михалёв, Я имел в виду не параллельные операции, а векторные.
Например, в данном примере что-то вроде этого:
cummax = np.maximum.accumulate(cashes)<br />drawdown = cashes - cummax<br />max_drawdown = drawdown.min()
В любом случае, numba тоже полезная штука.
avatar
Riskplayer, ну да, лучше использовать numpy где можно, оно там под капотом активно использует simd, но иногда numpy недостаточно. Это просто пример примитивный, а в реальности может быть какая-нибудь стейт машина, которая работает над несколькими массивами.
Riskplayer, Вот эта версия работает медленнее, чем на numba.

def vectorized(initial_cash, cashes):
    balances = np.concatenate([[initial_cash], cashes])
    cumulative_max = np.maximum.accumulate(balances)
    drawdowns = (cumulative_max — balances) / cumulative_max
    return np.max(drawdowns)



Работает в 7 медленнее. Оно и логично. Версия на numba — однопроходная, без выделений памяти на массивы, а это максимально эффективное использование L1 кэша. Векторизованнная версия имеет 4 векторных операции и 4 вспомогательных массива, под которые выделяется память. Даже если некоторые из 4 векторных операций используют simd, то использование лишних массивов и несколько проходов убивают производительность. Ну и сильно сомневаюсь, что функция np.maximum.accumulate имеет simd реализацию, т.к. по своей природе алгоритм последовательный. Сорри за занудство:)


Читайте на SMART-LAB:
Фото
📈 Как сравнивать конкурентов
Мультипликаторы — это показатели, которые помогают оценить стоимость компании и сравнить с показателями других игроков сектора....
Фото
Гранд-идея. Весь мир торгует металлами
Главная тема на финансовом рынке прямо сейчас — рекордная волатильность на рынке металлов. Хайп вокруг золота, серебра, платины и меди пришёл на...
Займер: спрос на займы заметно вырос в конце января 🔥
В рамках исследования для СМИ мы изучили спрос на займы в январе и получили любопытную статистику. 🟢 Оказалось, что в конце января люди...
Фото
Хэдхантер. Ситуация на рынке труда в январе. Хуже - чем просто хуже некуда
Вышла статистика рынка труда за январь 2026 года, которую Хедхантер публикует ежемесячно, что же там интересного: Динамика hh.индекса...

теги блога Михаил Михалёв

....все тэги



UPDONW
Новый дизайн