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 часто дешевле в поддержке.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте