Python удобен для экспериментов, но численные циклы в нём упираются в интерпретатор. Обычный путь — переписать горячий участок на C++ или CUDA, получить ускорение и вместе с ним вторую кодовую базу, сборку и отладку. Mojo предлагает другой вариант: синтаксис, знакомый по Python, статическая типизация, компиляция в нативный код и доступ к SIMD и потокам без внешних библиотек. Разберём, где язык реально полезен и какие ограничения учесть до переноса проекта.
Что такое Mojo
Mojo — компилируемый язык от Modular, построенный на MLIR и LLVM. Он задуман как язык с Python-подобным синтаксисом, контролем типов, владением значениями и предсказуемой производительностью. Компилятор генерирует нативный код, поэтому накладные расходы интерпретатора исчезают, а цикл по массиву может превратиться в векторные инструкции.
Ключевая идея — не заставлять переписывать всё. Mojo умеет вызывать Python-модули и работать с их объектами, так что тяжёлое ядро переносится постепенно, а обвязка остаётся на Python. При этом сам язык строже: типы указываются явно, изменяемость объявляется через var, а неизменяемые значения — через let.
Установка
Официальный способ — менеджер пакетов pixi, который ставит компилятор из канала Modular и не конфликтует с системным Python. После установки проверьте версию и наличие компилятора в PATH.
curl -fsSL https://pixi.sh/install.sh | bash
pixi global install -c https://conda.modular.com/max -c conda-forge mojo
mojo --version
pixi list -g | grep mojo
Если в организации запрещены установщики из сети, скачайте бинарник pixi вручную и положите в ~/.local/bin. Для редактора есть расширение VS Code с подсветкой, переходами и запуском. Отдельный пункт — CI: зафиксируйте версию Mojo в манифесте pixi, иначе обновление канала однажды сломает сборку.
Синтаксис: fn, def, типы
В Mojo две формы функций. fn требует аннотаций типов и явного raises, если функция может бросить исключение. def ближе к Python: типы необязательны, поведение динамическое. Для горячего кода используют fn — компилятору проще оптимизировать.
fn mean(values: List[Float64]) -> Float64:
var total: Float64 = 0.0
for v in values:
total += v
return total / len(values)
fn main():
let data = List[Float64](1.0, 2.0, 3.0, 4.0)
print(mean(data))
Обратите внимание на let и var: первое создаёт неизменяемую привязку, второе — изменяемую. Компилятор проверяет это на сборке, и часть ошибок, всплывающих в Python в рантайме, здесь видна сразу.
Владение и аргументы
В Mojo есть соглашения о передаче аргументов: borrowed (по умолчанию, только чтение), inout (изменение на месте) и owned (владение переходит к функции). Это не синтаксический сахар: от соглашения зависит, будет ли значение скопировано.
fn swap(inout a: Int, inout b: Int):
let tmp = a
a = b
b = tmp
fn main():
var x = 1
var y = 2
swap(x, y)
print(x, y)
Большие структуры выгодно передавать borrowed, а не копировать. Если функция не должна менять объект, не давайте ей inout.
SIMD без интринсиков
Векторные типы встроены в язык. SIMD[DType.float32, 8] — это восемь значений в одном регистре, и арифметика над ним компилируется в векторные инструкции. Такой код переносим между x86 и ARM, потому что за выбор инструкций отвечает бэкенд.
fn dot8(a: SIMD[DType.float32, 8], b: SIMD[DType.float32, 8]) -> Float32:
return (a * b).reduce_add()
fn main():
let a = SIMD[DType.float32, 8](1.0)
let b = SIMD[DType.float32, 8](2.0)
print(dot8(a, b))
Ширину вектора лучше выносить в параметр и подбирать под целевую платформу. На практике сначала пишут скалярную версию, замеряют, а потом векторизуют — иначе легко оптимизировать участок, который не влияет на общее время.
Параллелизм
Для циклов есть parallelize из модуля algorithm. Он раскладывает итерации по потокам, а тело оформляется вложенной функцией. Никаких пулов и futures для типового случая не требуется.
from algorithm import parallelize
fn scale(data: List[Float64]):
@parameter
fn work(i: Int):
data[i] = data[i] * 2.0
parallelize[work](len(data))
fn main():
var v = List[Float64](1.0, 2.0, 3.0, 4.0)
scale(v)
print(v)
Следите за гонками: parallelize не делает тело безопасным автоматически. Если итерации пишут в общий аккумулятор, результат будет зависеть от планировщика. Складывайте в локальные переменные и объединяйте после цикла.
Связка с Python
Взаимодействие идёт через Python и PythonObject. Можно импортировать модуль, вызвать функцию и вернуть результат обратно в Mojo-код. Это удобно для постепенной миграции: вход и выход остаются в Python, а узкое место переезжает в скомпилированную функцию.
from python import Python
fn main() raises:
let np = Python.import_module("numpy")
let arr = np.linspace(0.0, 1.0, 5)
print(arr.mean())
Цена перехода через границу ощутима: каждый вызов Python-объекта дороже нативной операции. Не стоит дёргать Python в цикле на миллион итераций — выигрыш от компиляции растворится в накладных расходах.
Сборка и инструменты
Обычный цикл разработки — mojo run, сборка — mojo build. Для библиотек есть mojo package, для форматирования — mojo format. Версию языка фиксируйте в проекте, потому что стандартная библиотека активно меняется.
mojo run hello.mojo
mojo build main.mojo -o app
mojo package src -o mylib.mojopkg
mojo format src/
Профилирование пока слабое место: инструментов меньше, чем у C++ или Rust. Начинайте с внешних замеров времени.
Где применять
- Численные ядра: свёртки, преобразования, обработка сигналов.
- Подготовка данных, где Python уже стал узким местом.
- Инференс небольших моделей и предобработка тензоров.
- Обучение и эксперименты с GPU через экосистему MAX.
Не стоит переносить на Mojo CRUD-сервисы, скрипты автоматизации и всё, где важна зрелость библиотек. Экосистема молодая, готовых пакетов мало, а документация местами отстаёт от кода.
Подводные камни
- Стандартная библиотека меняется между выпусками, старые примеры не компилируются.
- Не все Python-библиотеки работают через связку с Python без правок.
- Ошибки типов иногда выглядят непривычно — читайте сообщение целиком.
- Параллельные циклы требуют ручной проверки на гонки.
Итог
Mojo стоит рассматривать как инструмент точечного ускорения, а не как замену Python целиком. Начните с одного измеримого участка, сравните время до и после, проверьте сборку в CI. Если выигрыш есть и код остаётся читаемым, расширяйте область; если нет — Python с NumPy часто дешевле в поддержке.