Почему Julia быстрая: JIT и стабильность типов

Разбираемся, что именно делает Julia быстрой и как писать код, который раскрывает её потенциал.

Стабильность типов (type stability) — свойство функции, при котором тип возвращаемого значения зависит только от типов аргументов, а не от их значений. Это ключ к скорости в Julia.

Откуда берётся скорость

Когда функция вызывается с конкретными типами аргументов, Julia через LLVM компилирует специализированную версию в машинный код. Если компилятор может проследить типы всех промежуточных значений, он генерирует код, не уступающий C. Если же типы «плавают» (компилятор не может предсказать тип переменной), приходится откатываться к медленному «динамическому» режиму с проверками в рантайме.

Правило №1: оборачивайте код в функции

Код в глобальной области медленный, потому что тип глобальной переменной может измениться в любой момент. Внутри функции компилятор уверен в типах. Сравните:

# медленно: цикл в глобальной области
s = 0
for i in 1:1000
    global s += i
end

# быстро: тот же цикл внутри функции
function mysum(n)
    s = 0
    for i in 1:n
        s += i
    end
    return s
end
println(mysum(1000))

Вывод:

500500

Правило №2: пишите стабильно по типам

Функция нестабильна, если тип результата зависит от значения. Классический антипример:

# нестабильно: вернёт то Int (0), то Float64
bad(x) = x > 0 ? x : 0

# стабильно: всегда тот же тип, что у x
good(x) = x > 0 ? x : zero(x)

В bad литерал 0 — это Int, и если xFloat64, тип результата «прыгает». В good функция zero(x) возвращает ноль того же типа, что x, и тип стабилен.

Инструмент диагностики: @code_warntype

Макрос @code_warntype показывает, как Julia вывела типы внутри функции. Нестабильные места он подсвечивает красным (тип Any или объединение Union):

@code_warntype good(3.5)

Если в выводе все типы конкретны (нет Any, нет Union{Int64, Float64} в неожиданных местах), функция стабильна и быстра. Для замеров времени используют пакет BenchmarkTools и макрос @btime, который усредняет много прогонов и не учитывает время компиляции.

Как работает под капотом

JIT-конвейер Julia таков: исходный код → разбор → вывод типов → промежуточное представление → оптимизация LLVM → машинный код. Самый важный этап — вывод типов. Если он успешен, дальше LLVM применяет те же оптимизации, что и для C: инлайнинг, векторизацию (SIMD), устранение проверок границ. Если вывод типов «сломался» из-за нестабильности, генерируется обобщённый медленный код. Поэтому диагностика через @code_warntype — главный навык оптимизации в Julia.

Частые ошибки

Топ-ошибки замедления: (1) измерять скорость в глобальной области, а не в функции; (2) держать значения в нетипизированных глобальных переменных без const; (3) делать поля структур абстрактного типа; (4) нестабильные функции. Ещё частая ловушка новичка — мерить «скорость Julia», включая время первой компиляции; используйте @btime, который её отбрасывает.

Итоги

  • Скорость даёт JIT-компиляция через LLVM при успешном выводе типов.
  • Оборачивайте горячий код в функции — в глобальной области он медленный.
  • Пишите функции стабильными по типам (тип результата зависит только от типов аргументов).
  • @code_warntype диагностирует нестабильность; @btime из BenchmarkTools меряет скорость.
Проверьте себя
1. Почему цикл, написанный внутри функции, работает быстрее такого же цикла в глобальной области?
AФункции выполняются на видеокарте
BВнутри функции компилятор знает типы переменных и генерирует специализированный код
CГлобальный код вообще не компилируется
DЦиклы в Julia запрещены вне функций
2. Что такое «стабильность типов» функции?
AФункция никогда не меняет аргументы
BТип результата зависит только от типов аргументов, а не от их значений
CФункция возвращает только целые числа
DФункция не использует глобальные переменные
3. Зачем используют макрос @code_warntype?
AЧтобы запустить код на сервере
BЧтобы увидеть выведенные типы и найти нестабильные места (Any, Union)
CЧтобы перевести код в Python
DЧтобы отформатировать код