Блог им. 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 — это как писать на С++, только на питоне:)

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

avatar
Кому это реально нужно, кто это понимает, и так может за 10 минут решить парой запросов через вайб или устаревший промпт инженеринг (судя по тексту, оно именно так и составлено).

А кто в этом не шарит или не нужно, это как китайская грамота.

Для кого это всё пишется…
avatar

Комментарии не по делу: если хотите измерять перформанс с точностью лучше чем ± 1/2 слона, набирайте размер задачи на несколько секунд исполнения (для самой быстрой).


Также можно сильно уменьшить случайный шум, если делать 3-5 прогонов и брать наименьший результат прогона, а не средний.

А прогон в 0.0014 сек и результат с точностью 4 знака (137.6x) — это замер шума вселенной.

avatar
Кирилл Гудков, тут не нужны были точные замеры, нужно было просто показать, что легким движением рук код на питоне ускоряется во много раз.
avatar
Михаил Михалёв, когда аргументы заканчиваются, переходят на личность оппонента.
avatar

Читайте на SMART-LAB:
Фото
Скидка 15% на нашу аналитику — только 72 часа!
Увеличь доходность своего портфеля с профессиональной командой аналитиков. Наши идеи уже принесли клиентам прибыль с начала года. Ты мог...
Фото
GBP/USD: "Падающая звезда" засверкала над руинами тренда
«Старый джентльмен» пробил линию восходящего тренда и уровня поддержки 1.3508. В настоящий момент цена протестировала точку пересечения этих...
Brent нацелена на штурм отметки $75
Нефть марки Brent дорожает на 0,74%, до $71,34 за баррель, цена сорта WTI повысилась на 0,84%, до $67,04. На этой неделе продолжатся переговоры...
Фото
Длинные ОФЗ: зарабатываем как по ВДО
ЦБ РФ 13 февраля в очередной раз снизил ключевую ставку до 15,5%, тем самым продолжив тренд смягчения ДКП (кумулятивное снижение с июня 2025 г....

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

....все тэги



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