Блог им. MihailMihalev
Пишешь код на 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.jit Numba скомпилирует функцию через LLVM в машинный код. Скорость сравнима с ручным C++. Ускорение — 100-200x.
Что под капотом: Numba берёт байт-код python функции, строит LLVM IR, применяет агрессивные оптимизации: инлайнинг, векторизацию SIMD инструкций, оптимизацию доступа к памяти. Всё это — тот же туллинг, что использует Clang (C++ компилятор на LLVM).
Где применять: любые плотные вычисления над массивами — кастомные индикаторы, статистические расчёты, симуляции, фильтрация данных.
Ограничения: внутри @numba.jit — только NumPy, примитивные типы, math.*, часть numpy.*. Нет pandas, нет произвольных Python-объектов.
Numba — это как писать на С++, только на питоне:)
Например, в данном примере что-то вроде этого:
В любом случае, 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 реализацию, т.к. по своей природе алгоритм последовательный. Сорри за занудство:)