Вышла Julia 1.13: прекомпиляция ускорилась на 30%, а старт на 20%
Проект Julia выпустил версию 1.13 языка программирования: релиз в блоге описывают как набор изменений от скорости запуска и прекомпиляции пакетов до нового алгоритма хеширования и переработанной консоли REPL. Прекомпиляция пакетов идёт примерно на 30% быстрее, чем в версии 1.12, и примерно на 10, 20% быстрее, чем в LTS-ветке 1.10 (разброс зависит от машины). Замер идёт по 39 сценариям, которые сообщество прислало в проект Julia-TTFX-Snippets; с 7 сентября 2026 года их результаты публично отслеживаются на странице perf.julialang.org/ttfx, а сама проверка TTFX (time to first X, сумма времени на прекомпиляцию, загрузку и выполнение) теперь автоматически запускается на релевантные pull request и на каждый коммит в основную ветку Julia.
Старт самого интерпретатора Julia 1.13 тоже ускорился примерно на 20% относительно 1.12. Замер утилитой hyperfine (20 запусков, 3 разогревочных) даёт среднее 56,7 мс против 69,1 мс у 1.12, то есть в 1,22 раза быстрее.
Консоль REPL получила встроенную подсветку синтаксиса: раньше для этого требовался сторонний пакет OhMyREPL.jl. Цветовая схема по умолчанию сдержанная, но настраивается; в блоге в качестве примера показана схема Monokai. Поиск по истории команд (по умолчанию вызывается через Ctrl-R) переработан и теперь похож на консольный нечёткий поисковик fzf: он ищет по подстрокам нечётко, показывает, в каком режиме REPL была введена команда, позволяет выбрать сразу несколько результатов для вставки в строку ввода и подсвечивает синтаксис так же, как основная консоль. Ещё одно изменение: механизм bracketed paste. Он позволяет приложению в терминале понимать, что текст был вставлен, а не напечатан вручную, и обрабатывать его быстрее и правильнее. На Linux и macOS это работало давно, а в 1.13 функция наконец добавлена и для Windows.
В язык добавлен макрос @FUNCTION: по аналогии с уже существующими @MODULE и @FILE он возвращает ближайшую содержащую его функцию, даже если та анонимная. В блоге за это изменение указаны Майлз Крэнмер и Джефф Безансон. Разработчики подчёркивают: макрос должен работать в функциях любого вида и, в отличие от служебной внутренней переменной #self#, официально входит в публичный API.
Заменена и хеш-функция: побайтовый алгоритм хеширования теперь называется RapidhashNano и пришёл на смену MurmurHash3. Он используется по умолчанию для строк (AbstractString) и ряда числовых типов вроде BigInt, Rational и больших Real/Integer, переписан с C на чистый Julia ради читаемости и поддержки, и, в отличие от предыдущей реализации, ему не нужно заранее знать длину входных данных (потоковое хеширование). На длинной строке (скачанный с Project Gutenberg текст) вызов hash() ускорился с 8,555 до 1,742 мкс, почти в 4,9 раза. Авторские типы данных могут получить ещё больший выигрыш, реализовав методы codeunit и codeunits вместо конвертации в String: в примере из блога это ускорило hash() с 204,583 до 1,750 мкс, почти в 117 раз. У самого шага смешивания хешей тоже поменялась конструкция (однораундовая схема XMX с новыми константами), из-за чего появилась зависимость по данным: при последовательном хешировании элементов в тесном цикле (например, foldr(hash, collection)) производительность может, наоборот, просесть, зато хеширование массивов (AbstractArray) частично развёрнуто и в большинстве случаев станет заметно быстрее. Хеш остаётся некриптографическим, а дефолтный seed изменился. Разработчики отдельно советуют: собственные методы hash должны всегда принимать seed аргументом и никогда не задавать для него значение по умолчанию, потому что правильное значение определяет вызывающий код.
Изменения затронули и сборщик мусора. Каждая сессия Julia стартует с большим числом объектов, загруженных из системного образа (sysimage) и образов пакетов: таблиц методов, информации о типах, скомпилированного кода и констант. Раньше полная сборка мусора обходила все эти объекты, чтобы пометить их как достижимые, хотя они никогда не освобождаются и лишь изредка изменяются; при нескольких крупных загруженных пакетах именно это часто и было основной статьёй расходов полной сборки. В версии 1.13 такие объекты помечаются достижимыми один раз и навсегда, и фаза разметки больше их не обходит; редкие изменения самих объектов образа (например, при добавлении метода к существующей функции) отслеживаются отдельно. В итоге стоимость полной сборки теперь масштабируется с размером кучи, которую реально создала программа, а не с объёмом загруженного кода. В свежей сессии время полной сборки (GC.gc()) упало с 35,493 до 0,528 мс, примерно в 67 раз. На более практичном примере функция раз за разом вставляет случайные векторы в Dict размером до 50 тысяч ключей (всего 5 миллионов операций вставки за один прогон), и поскольку сам Dict остаётся живым между итерациями, часть его данных переходит в старое поколение кучи: общее время выполнения сократилось с 1,70 до 0,57 секунды, то есть втрое, а доля этого времени, уходящая на сборку мусора, снизилась с 79,80% до 44,32%. Инкрементальные сборки (для молодого поколения) это изменение не затрагивает: они одинаково быстры в обеих версиях.
Планировщик потоков тоже переработан: простаивающие потоки теперь паркуются в отдельной служебной задаче, а не удерживают последнюю выполненную задачу, поэтому завершённые задачи вовремя освобождаются сборщиком мусора. Заодно исправлены прерывания: Ctrl-C снова доходит до пользовательского кода, включая скрипты, застрявшие в sleep или операциях ввода-вывода, и вновь работает Distributed.interrupt; REPL переживает повторные и неудачно нажатые Ctrl-C. Макрос @spawn теперь будит только один простаивающий поток из пула задачи, а не все сразу: на 16-ядерной Linux-машине это ускоряет код, интенсивно порождающий задачи, в 1,1, 1,6 раза, на Windows и сильно перегруженных задачами машинах, в 10, 300 раз (там пробуждение всех потоков было основной статьёй расходов), а на macOS изменений почти нет. Устранён и ряд состояний гонки, приводивших к потере задач и взаимным блокировкам. Полноценный механизм отмены задач разработчики называют незавершённым: он в работе и запланирован на версию Julia 1.14.
Макросы для интроспекции кода, @which, @code_typed, @code_warntype и другие, научились принимать в вызовах типы вместо значений аргументов (тот же синтаксис ::T, что и в определениях методов и в трассировках стека), причём типы и значения можно свободно смешивать, а именованные аргументы поддерживаются. Это значит, что кадр из трассировки стека можно скопировать прямо в @which и найти вызванный метод. Такая же поддержка типов появилась и для выражений с broadcast-синтаксисом в @code_lowered, @code_typed и @code_warntype. Новый флаг командной строки --trace-eval печатает ход вычисления верхнеуровневых выражений: удобно, чтобы понять, как продвигается тестовый набор или скрипт, и найти зависание. Флаг включается автоматически, когда в CI-прогоне (например, в GitHub Actions) включено отладочное логирование. Скрипт juliac.jl из репозитория Julia превращён в полноценный пакет-приложение JuliaC.jl: он умеет обрезать (trim) больше видов кода, включая финализаторы, @cfunction и mapreduce, а несколько багов самого механизма обрезки исправлены для надёжности.
Пакетный менеджер Pkg тоже получил изменения. Главное: при загрузке с сервера пакетов (реестров, самих пакетов и артефактов) Pkg теперь по умолчанию запрашивает архив, сжатый zstd, вместо gzip: для файлов, которые обычно скачивает Pkg, zstd даёт как лучшее сжатие, так и заметно более быструю распаковку. Резолвер зависимостей и обработка реестра тоже получили точечные оптимизации, которые в целом ускоряют операции Pkg.
Ключевые факты
- Прекомпиляция пакетов в Julia 1.13 идёт примерно на 30% быстрее, чем в 1.12, и примерно на 10, 20% быстрее, чем в LTS-ветке 1.10 (в зависимости от машины); замер идёт по 39 сценариям сообщества и с 7 сентября 2026 года публикуется на perf.julialang.org/ttfx.
- Старт интерпретатора ускорился примерно на 20%: hyperfine показал среднее 56,7 мс против 69,1 мс у 1.12, то есть в 1,22 раза быстрее.
- Хеш-функция заменена на RapidhashNano вместо MurmurHash3: на длинной строке hash() ускорился почти в 4,9 раза, а у авторских строковых типов, реализовавших методы codeunit и codeunits, почти в 117 раз.
- Сборщик мусора больше не пересканирует объекты системного образа и образов пакетов при полной сборке: время GC.gc() в свежей сессии упало с 35,493 до 0,528 мс, а в тесте с 5 миллионами вставок в Dict общее время сократилось втрое (с 1,70 до 0,57 секунды).
- REPL получил встроенную подсветку синтаксиса и нечёткий поиск по истории в духе fzf (Ctrl-R); макрос @spawn теперь будит один поток вместо всех: на Windows и перегруженных задачами машинах это ускоряет код, интенсивно порождающий задачи, в 10, 300 раз.
Почему это важно
Julia проектировали для научных и численных вычислений, но время до первого результата (Time To First X, TTFX: сумма прекомпиляции, загрузки и выполнения) долгие годы было её слабым местом. Релиз 1.13 бьёт именно в эту точку: прекомпиляция пакетов быстрее примерно на 30%, старт интерпретатора примерно на 20%, а сборщик мусора, вместо того чтобы на каждой полной сборке пересматривать весь загруженный код, теперь помнит, что объекты системного образа и пакетов никогда не освобождаются, и пропускает их. На тесте с длительной работой и большой живой кучей это дало трёхкратное ускорение. Вместе с этим Julia формализовала измерение TTFX: с 7 сентября 2026 года данные по 39 сценариям сообщества публикуются на отдельной странице и проверяются на релевантные pull request и на каждый коммит в основную ветку, то есть регресс производительности видно раньше, чем он попадёт в релиз.
Кому это важно
В первую очередь тем, кто уже пишет на Julia: исследователям и инженерам в численных вычислениях и дата-сайентистам, которые годами платят долгой прекомпиляцией и стартом за интерактивную работу в REPL. Отдельно выигрывают пользователи Windows: там же, где раньше пробуждение всех потоков при @spawn было основным тормозом, разработчики зафиксировали ускорение в 10, 300 раз, и там же наконец появилась поддержка bracketed paste, которая на Linux и macOS работала уже давно. Авторам пакетов, которые определяют собственные строковые типы или методы hash, стоит обратить внимание на новую хеш-функцию и совет про seed. Тем, кто гоняет тесты и скрипты в CI, пригодится флаг --trace-eval для поиска зависаний.
Как это применить
Большинство улучшений (прекомпиляция, старт, сборка мусора, исправления планировщика и прерываний) работают сами по себе после обновления до Julia 1.13, никакого кода менять не нужно. Точечно можно выжать больше: если у вас есть собственный тип-наследник AbstractString, реализация методов codeunit и codeunits вместо перевода в String тоже даёт прирост скорости хеширования: в примере разработчиков это почти 117-кратное ускорение, а базовый переход на RapidhashNano для обычной String даёт почти 4,9-кратное. Если вы пишете собственные методы hash, их сигнатура должна всегда явно принимать seed и не задавать для него значение по умолчанию: правильное значение передаёт вызывающий код. В REPL сразу доступны подсветка синтаксиса (со своей палитрой, например Monokai) и переработанный поиск по истории через Ctrl-R с нечётким поиском и показом режима. Пользователям скрипта juliac.jl нужно перейти на новый пакет JuliaC.jl. В CI флаг --trace-eval включается сам при включённом отладочном логировании (например, в GitHub Actions); можно также добавить его вручную, чтобы найти зависший шаг.
Можно ли доверять
Источником служит официальный блог проекта Julia, а не пересказ третьей стороны: цифры подкреплены точными командами и полным выводом бенчмарков (hyperfine для старта, @time GC.gc() для сборщика мусора, @btime hash для хеш-функции), а не округлёнными процентами без контекста. Доверие подкрепляет и то, что авторы не только хвалят изменения, но и честно называют побочный эффект: новая схема смешивания хешей может замедлить последовательное хеширование в тесном цикле вроде foldr(hash, collection), а ускорение от правки в @spawn на macOS практически нулевое, авторы не приукрашивают результат ради красивой картины. Отдельный плюс: систематический и публичный трекинг TTFX на perf.julialang.org/ttfx, который с 7 сентября 2026 года проверяет производительность на релевантные pull request и на каждый коммит в основную ветку, а не только перед релизом. Ограничение в том, что это самоотчёт разработчиков: независимой стороны, перепроверившей бенчмарки на своём железе, в источнике нет.
Риски и подводные камни
Не всё в 1.13 обходится без компромиссов. У новой схемы смешивания хешей есть побочный эффект: она вводит зависимость по данным, из-за которой последовательное хеширование элементов в тесном цикле (например, foldr(hash, collection)) может, наоборот, замедлиться: выигрыш для основных сценариев (массивы, строки) не гарантирует выигрыша для всех. Дефолтный seed для hash тоже изменился, а сам hash остаётся некриптографическим. Полноценный механизм отмены задач (task cancellation) в этом релизе не появился: он только в разработке и запланирован на версию 1.14, так что штатного способа прервать зависшую задачу пока по-прежнему нет.