7-го мая 2023, после 3-х бета-версий и 3-х релиз-кандидатов наконец выпущена новая версия языка программирования Julia 1.9. Мы хотели бы поблагодарить всех участников, разработчиков этого выпуска и всех тех, кто тестировал и помог выявить проблемы в предварительных выпусках. Без вас этот выпуск был бы невозможен.

Полный список изменений можно найти в файле release-1.9/NEWS.md, а в этой заметке мы дадим обзор некоторых ключевых моментов выпуска.

Кэширование машинного кода

Tim Holy, Jameson Nash, and Valentin Churavy

Предварительная компиляция в Julia включает в себя llvm-компиляцию кода пакета и сохранение результата на диск в виде кэш-файла индивидуально для каждого пакета и каждой версии пакета. Этот процесс значительно сокращает время, необходимое для последующей компиляции кода, который использует этот пакет, так как повторная компиляция самого пакета больше не требуется.

Однако до версии Julia 1.9 можно было сохранить только часть скомпилированного кода: сохранялись типы, переменные, методы (llvm-код) и результаты вывода типов аргументов, для методов, которые разработчики пакетов разметили с помощью precompile. Примечательно, что в файлах кеша отсутствовал машинный код, то есть код, который фактически исполняется на целевом процессоре. Хотя кэширование помогало уменьшить задержку времени до момента первого выполнения (TTFX - time-to-first-execution), необходимость генерировать машинный код в каждом сеансе запуска процесса Julia означала, что многие пакеты по-прежнему имели большую величину задержки TTFX, что очень раздражала пользователей Julia.

Начиная с версии Julia 1.9 доступно кэширование именно машинного кода. Это привело к значительному снижению задержки TTFX и положило путь к улучшению всей экосистемы пакетов. Теперь авторам пакетов доступны как выражение precompile, так и пакет PrecompileTools для предварительного кэширования ответственных методов. Пользователи также могут создавать собственные адаптированные "Startup"-пакеты, которые включают все необходимые зависимости и предварительно компилируют рабочий кода.

Эта функция реализована с некоторыми компромиссами, например время прекомпиляции увеличилось на 10%-50%. Однако, поскольку это происходит однократно, мы считаем, что компромисс того стоит. Кэш-файлы также стали больше из-за хранения большего количества данных и использования другого формата сериализации.

На приведенном ниже графике показаны изменения времени загрузки (TTL - time-to-load) , TTFX и размера файла кеша для Julia 1.7, 1.8 и 1.9:

Детали измерения см. ниже. Для большинства пакетов TTFX превратился из доминирующего фактора задержки в практически незначительный. Значение TTL также уменьшилось, хотя и не так сильно, как TTFX. Те же данные представлены в таблице ниже, где столбцы «ratio» показывают во сколько раз Julia 1.9 быстрее Julia 1.7, а «total» означает «TTL + TTFX».

package

TTFX 1.7 [s]

TTFX 1.9 [s]

TTFX ratio

TTL 1.7 [s]

TTL 1.9 [s]

TTL ratio

total ratio

CSV

11.66

0.08

137.43

0.38

0.65

0.59

16.34

DataFrames

17.39

0.38

45.99

1.59

1.6

0.99

9.58

Revise

7.04

0.39

18.2

2.95

2.38

1.24

3.61

GLMakie

64.62

1.66

39.02

12.21

9.47

1.29

6.91

LV

11.06

0.0

7677.8

2.56

0.97

2.64

14.05

OrdinaryDiffEq

2.25

0.21

10.67

9.95

5.95

1.67

1.98

ModelingToolkit

73.53

4.81

15.3

20.93

13.5

1.55

5.16

JuMP

10.36

0.28

36.75

4.68

3.96

1.18

3.54

ImageFiltering

1.35

0.12

10.84

2.76

2.14

1.29

1.82

Эти цифры свидетельствуют о значительном улучшении времени загрузки для различных пакетов.

Вместе с пакетом PrecompileTools.jl Julia 1.9 предоставляет возможность использования PackageCompiler не требуя дополнительной настройки. Ниже приводится сравнение сравнение их использования:

Цель

Julia 1.9 + PrecompileTools

PackageCompiler

Разработчики пакетов могут уменьшить TTFX для своих пользователей

✔️

Пользователи пакетов могут уменьшить TTFX

✔️

✔️

Пакеты могут быть обновлены без необходимости полной пересборки системного образа

✔️

Уменьшить TTL - время загрузки машинного кода

✔️

Разница по времени TTL возникает из-за того, что загрузка системного машинного образа может пропускать проверки кода, которые необходимы при загрузке пакетов индивидуально.

На момент выпуска Julia 1.9 разработчики самых популярных пакетов уже начали использовать средства PrecompileTools. По мере расширения использования этих новых инструментов, конечные пользователи могут ожидать постоянных улучшений в TTFX.

Методика измерения

Для каждого пакета была разработана "рабочая нагрузка", то есть тот код, который в норме должен выполняться с этим пакетом. Этот код был помещен в Startup-пакет и предварительно скомпилирована; для бенчмаркинга мы загружаем Startup-пакет и запускаем ту же рабочую нагрузку. Полную информацию можно найти в этом репозитории.

Package extensions

Kristoffer Carlsson

Возможности множественной диспетчеризации в Julia позволяют легко расширять функцию для широкого диапазона типов. Например, пакет построения графиков может предоставить возможности для работы с различными объектами Julia, многие из которых определены в отдельных пакетах в рамках экосистемы Julia. Кроме того, можно добавлять оптимизированные версии функций (методы в терминологии Julia) для конкретных типов, таких как StaticArray, где размер массива известен во время компиляции, что приводит к значительному повышению производительности.

Чтобы расширить метод до нужного типа, необходимо импортировать пакет, содержащий тип, а затем определить расширенный метод. На примере пакета plotting, это может выглядеть примерно так:

import Contours

function plot(contour::Countours.Contour)
    ...
end

Однако добавление зависимостей в пакет может потребовать дополнительных затрат, включая увеличение времени загрузки или установку больших артефактов (например, это присутствует в CUDA.jl). Это может быть обременительно для авторов пакетов, которые должны постоянно балансировать между затратами на внешние зависимости с преимуществами новых расширенных методов и потребностями «среднего» пользователя пакета.

В Julia 1.9 появился механизм «package extensions», который в широком смысле автоматически загружает модуль при загрузке определённого набора пакетов. Модуль, содержащийся в файле в каталоге ext родительского пакета, загружает «слабую зависимость» и дополнительные методы. Цель состоит в том, чтобы не платить за функции, которые не используются. Механизм расширения пакетов предоставляет функциональные возможности, аналогичные тем, которые уже доступны с пакетом Requires.jl, но с такими ключевыми преимуществами, как возможность предварительной компиляции условного кода и добавление ограничений совместимости для слабых зависимостей. Поскольку механизм расширения пакетов теперь поддерживается системно, авторам пакетов следует предпочесть его использованея вместо Requires.jl. "Расширения пакетов" могут быть добавлены в пакет с обеспечением обратной совместимости, включая либо выбор варианта Requires.jl, либо слабую зависимость как полноценную реализацию для предыдущих версий Julia.

В качестве конкретного примера, когда «package extensions» используется для достижения существенного эффекта, пакет ForwardDiff.jl который предоставляет оптимизированные подпрограммы для автоматической дифференциации, и случая, когда входными данными являются экземпляры StaticArray. В Julia 1.8 он безоговорочно загружал пакет StaticArrays, а в 1.9 использует «package extensions». Это приводит к значительному снижению времени загрузки:

# 1.8 (StaticArrays unconditionally loaded)
julia> @time using ForwardDiff
  0.590685 seconds (2.76 M allocations: 201.567 MiB)

# 1.9 (StaticArrays not loaded)
julia>  @time using ForwardDiff
  0.247568 seconds (220.93 k allocations: 13.793 MiB)

Полное руководство по использованию расширений пакетов см. в документации.

Heap snapshot

Apaz-Cli, Pete Vilter, Nathan Daly, Valentin Churavy, Gabriel Baraldi, and Ian Butterworth

Вам интересно, как ваша оперативная память используется в программах на Julia? С появлением Julia 1.9 теперь можно создавать снимки памяти и исследовать с помощью Chrome DevTools.

Чтобы создать снимок памяти, необходимо использовать пакет Profile и вызвать функцию take_heap_snapshot, например как показано ниже:

using Profile
Profile.take_heap_snapshot("Snapshot.heapsnapshot")

Если больше интересует количество объектов, а не их размеры, можно использовать аргумент all_one=true. Это установит размер каждого объекта в единицу, что упростит определение общего количества сохраненных объектов.

Profile.take_heap_snapshot("Snapshot.heapsnapshot", all_one=true)

Чтобы выполнить анализ памяти, откройте браузер Chromium и выполните следующие действия: right click -> inspect -> memory -> load. Загрузите файл c именем Snapshot.heapsnapshot, и слева появится новая вкладка для отображения сведений о вашем снимке.

Подсказка для сборщика мусора по использованию памяти --heap-size-hint

Roman Samarev

В Julia 1.9 представлен новый аргумент командной строки --heap-size-hint=<size>, который позволяет пользователям установить порог по использованию оперативной памяти, после чего сборщик мусора (GC) будет работать более агрессивно, чтобы очистить высвободить неиспользуемые объекты.

Этот порог не является жестким. Фактически может быть использовано больше оперативной памяти чем указано, но после его достижения, сборщик мусора начинает активнее управлять ресурсами памяти (потребляя больше ресурсов процессора и существенно чаще, чем обычно), но снижая риск нехватки памяти в целом.

Чтобы использовать эту новую функцию, надо просто запустить Julia с флагом --heap-size-hint, за которым следует желаемый лимит памяти:

julia --heap-size-hint=<size>

Замените <size> соответствующим значением (например, 1G для 1 гигабайта или 512M для 512 мегабайт или 10000k для килобайт).

Эта функция в Julia 1.9 упрощает эффективное управление ресурсами памяти как на локальных компьютерах, так и в контейнерах кластеров, предоставляя больший контроль и гибкость настройки при работе с приложениями, интенсивно использующими память.

Функция представлена в #45369.

Производительность сортировки

Lilith Hafner

Алгоритм сортировки по умолчанию был обновлен до адаптивного алгоритма сортировки, который стабилен по времени и обеспечивает высокую производительность. Для простых типов — BitInteger, IEEEFloat и Char для сортировки по умолчанию или в обратном порядке используется поразрядная сортировка (radix sort), которая имеет линейное время выполнения по отношению к размеру входных данных. Этот эффект особенно заметен для массивов типа Float16, где получено ускорение в 3-50 раз по сравнению с реализацией в Julia 1.8.

Для других типов алгоритм сортировки по умолчанию в большинстве случаев заменен на ScratchQuickSort, который является время-стабильным и в целом быстрее, чем QuickSort, хотя и имеет большие затраты памяти. В ситуациях, когда эффективность использования памяти имеет решающее значение, возможно переопределить алгоритмы сортировки по умолчанию, указав alg=QuickSort.

Узнать больше об этих изменениях можно в докладе на JuliaCon 2022, а также в предстоящем продолжении на конференции JuliaCon 2023.

Задачи и интерактивный пул потоков

Jeff Bezanson, Kiran Pamnany, Jameson Nash

До версии Julia 1.9 относилась ко всем задачам одинаково, запуская их во всех доступных потоках без различия приоритетов. Однако бывают ситуации, когда необходимо, чтобы определенные задачи были приоритетными, например, оповещение состояния для интерактивного интерфейса или отображении хода выполнения.

Чтобы реализовать это, задачу можно пометить как интерактивную в момент её создания с помощью Threads.@spawn:

using Base.Threads
@spawn :interactive f()

Количество доступных интерактивных потоков может быть установлено с помощью следующей команды:

julia --threads 3,1

Эта команда запускает Julia с тремя «обычными» потоками и одним интерактивным потоком (в пуле интерактивных потоков).

Для получения дополнительной информации обратитесь к разделу руководства по многопоточности. Эта функция представлена в #42302.

REPL

Контекст модуля в REPL

Rafael Fourquet

REPL по умолчанию в Julia рассматривает выражения как относящиеся к модулю Main. Начиная с версии 1.9, можно изменить этот контекст на любой другой модуль. Многие средства анализа, такие как varinfo, которые ранее по умолчанию проверяли только модуль Main, теперь по будут использовать текущий контекстный модуль REPL.

Эта функция может быть особенно полезна в момент разработки пакета, поскольку именно его можно установить в качестве текущего контекста. Чтобы изменить модуль, необходимо ввести имя модуля в REPL и нажать комбинацию клавиш Meta+M (или Alt+M) или использовать команду REPL.activate.

julia> @__MODULE__ # Shows module where macro is expanded
Main

# Typing Base.Math and pressing Meta-m changes the context module
(Base.Math) julia> @__MODULE__
Base.Math

(Base.Math) julia> varinfo()
  name           size summary
  ––––––––––– ––––––– –––––––––––––––––––––––––––––––––––––––––––––
  @evalpoly   0 bytes @evalpoly (macro with 1 method)
  Math                Module
  ^           0 bytes ^ (generic function with 68 methods)
  acos        0 bytes acos (generic function with 12 methods)
  acosd       0 bytes acosd (generic function with 1 method)
  acosh       0 bytes acosh (generic function with 12 methods)
  acot        0 bytes acot (generic function with 4 methods)
...

Нумерованные подсказки

Kristoffer Carlsson

В значительной степени вдохновленный оболочкой IPython (и другими системами на основе блокнотов типа Mathematica), REPL Julia теперь предоставляет «нумерованную подсказку», которая сохраняет результаты выполнения в REPL для последующего их использования по номеру и отслеживает общее количество выражений.

Возможность сослаться на ранее оцененный объект может быть полезна, если, например, во время интерактивной работы кто-то забыл сохранить результат длительного вычисления в переменную, а затем выполняет что-то еще (переменная ans при этом перезаписывается).

Инструкции по включению этого см. в документации.

DelimitedFiles — первая обновляемая стандартная библиотека

Kristoffer Carlsson

Julia поставляется с набором стандартных библиотек ("stdlibs"), которые похожи на обычные пакеты, за исключением того, что их можно загружать без необходимости явной установки. Большинство этих stdlib также поставляются «предварительно запеченными» в образ sysimage, который использует Julia, что означает, что технически все они загружаются каждый раз при запуске Julia.

Однако у этого подхода есть некоторые недостатки:

  • Версии stdlib привязаны к версии Julia, поэтому пользователям необходимо дождаться следующего выпуска Julia, чтобы получить для них исправления ошибок.

  • Предварительное включение stdlib в sysimage влечет за собой затраты времени и памяти для пользователей, которые их не используют, поскольку они в любом случае загружаются при каждом запуске Julia.

  • Разработка stdlib, пакетов, вшитых в sysimage, является неудобной для самих разработчиков пакетов.

В версии 1.9 мы экспериментируем с новой концепцией «обновляемых стандартных библиотек», которые поставляются вместе с Julia, но также могут быть обновлены как обычные пакеты. Начнем с того, что это делается с помощью небольшой и относительно редко используемой stdlib DelimitedFiles.

Начав с чистой установки Julia, мы видим, что пакет DelimitedFiles доступен и загружается из директории установки Julia:

julia> using DelimitedFiles
[ Info: Precompiling DelimitedFiles [8bb1440f-4735-579b-a4ab-409b98df4dab]

julia> pkgdir(DelimitedFiles)
"/Users/kc/julia/share/julia/stdlib/v1.9/DelimitedFiles"

Однако, когда DelimitedFiles добавляется с помощью диспетчера пакетов, то устанавливается конкретная версия пакета и выглядит так же, как и обычный пакет:

(@v1.9) pkg> add DelimitedFiles
   Resolving package versions...
    Updating `~/.julia/environments/v1.9/Project.toml`
  [8bb1440f] + DelimitedFiles v1.9.1
    Updating `~/.julia/environments/v1.9/Manifest.toml`
  [8bb1440f] + DelimitedFiles v1.9.1
Precompiling environment...
  1 dependency successfully precompiled in 1 seconds. 59 already precompiled.

julia> using DelimitedFiles

julia> pkgdir(DelimitedFiles)
"/Users/kristoffercarlsson/.julia/packages/DelimitedFiles/aGcsu"

Отключен режим --math-mode=fast

--math-mode=fast больше не работает (#41638). Мы пришли к выводу, что глобальную опцию fastmath невозможно корректно использовать в Julia. Например, это может привести к таким неожиданностям, как функция exp, возвращающая совершенно неверное значение, как указано в #41592. Сочетание опции быстрой математики во время выполнения с предварительной компиляцией и перекомпиляция по ходу выполнения приводит к рассогласованиям, которые могут быть устранены только если будет создан отдельный образ системы.

Вместо этого пользователям рекомендуется использовать макрос @fastmath, который ограничивает эффекты fastmath небольшим участком кода.

Pkg

pkg> up Foo обновляет только Foo

Kristoffer Carlsson

Ранее не указывалось, какие пакеты на самом деле разрешалось обновлять при запросе конкретного пакета на обновление (pkg> up Foo). Теперь up Foo позволяет обновляться только конкретному пакету Foo, но фиксируя версии пакетов, от которых он зависит. Можно ослабить это ограничение с помощью дополнительного аргумента --preserve, чтобы разрешить, обновления и зависимостей этого пакета Foo. См. документацию для Pkg.update для получения дополнительной информации.

pkg> add теперь обновляет реестр только раз в день

Kristoffer Carlsson

Теперь Pkg запоминает время последнего обновления общего реестра пакетов в сеансах julia, а при использовани команды add выполняет автоматическое обновление только один раз в день. Ранее реестр автоматически обновлялся один раз за сеанс. Обратите внимание, что команда update, как и прежде, всегда будет пытаться обновить реестр.

pkg> add теперь проверяет ранее установленные пакеты

Ian Butterworth

При работе со многими средами разработки, например в блокнотах Pluto, поведение Pkg.add по умолчанию предполагает, что добавление последней версии пакета и любых новых зависимостей вызывает выполнение предварительной компиляции.

Теперь можно указать Pkg.add добавлять уже установленные версии пакетов (те, которые уже были загружены на вашу машину), которые, скорее всего, будут предварительно скомпилированы.

Чтобы глобально установить этот режим, необходимо установить переменную окружения JULIA_PKG_PRESERVE_TIERED_INSTALLED в значение true.

Или, для разового использования добавить режим в команду добавления:

  • pkg> add --preserve=tiered_installed Foo для попытки иерархического разрешения зависимостей.

  • pkg> add --preserve=installed Foo жестко использовать эту стратегию или выдать ошибку.

Обратите внимание, что с помощью этого режима установки можно установить старые версии пакетов, поэтому, если возникают проблемы, рекомендуется выполнить фактическое обновление до последней версии, чтобы увидеть, была ли проблема исправлена.

pkg> why выдаёт причину почему пакет находится в манифесте

Kristoffer Carlsson

Чтобы показать причину, по которой пакет находится в манифесте, реализована новая команда pkg> why Foo. Результатом выполнения являются все цепочки доступа к пакету через граф зависимостей, начиная с прямых зависимостей.

(jl_zMxmBY) pkg> why DataAPI
  CSV → PooledArrays → DataAPI
  CSV → Tables → DataAPI
  CSV → WeakRefStrings → DataAPI

pkg> test --coverage (по умолчанию для CI) существенно ускорен

Ian Butterworth

Ранее тестирование покрытия имело три режима: all, где проверялся весь реально выполенный код, user (предыдущее значение по умолчанию для Pkg.test), где проверялось все, кроме Baseи stdlibs, или none, где отслеживание вообще отключено.

GitHub-action julia-runtest по умолчанию включает тестирование покрытия, поэтому ранее выполнялось большое количество работы за пределами тестируемого пакета, а это замедляет тесты.

Julia 1.8 представила возможность указать путь к файлу или каталогу для отслеживания покрытия с помощью --code-coverage=@path, а Julia 1.9 делает режим по умолчанию для Pkg.test(coverage=true) и поэтому используется GitHub-action julia-runtest.

Это означает, что будет требоваться отслеживать намного меньше кода, а в случаях, когда код из зависимостей имеет сложные и долгие циклы, это может значительно ускорить прохождение тестов. В одном из примеров тесты Octavian.jl с этим новым режимом покрытия конкретного пакета время тестирования уменьшилось с более чем 2 часов до ~ 6 минут.

Apple Silicon получает статус поддержки 1

После успешного прохождения всех тестов и конфигурирования непрерывной интеграции (CI) для Apple Silicon статус платформы был повышен с уровня 2 до уровня 1.

LLVM обновлён до версии 14

Valentin Churavy, Mosè Giordano

LLVM — это базовая инфраструктура компилятора, на которой строится компилятор Julia. В Julia 1.9 LLVM обновлён до 14.0.6.

Среди новых функций, представленных в LLVM 14, автовекторизация включена по умолчанию для расширений SVE/SVE2 на процессорах архитектуры AArch64. SVE, масштабируемое векторное расширение, представляет собой SIMD-подобное расширение, которое использует векторные регистры с изменяемой шириной вместо регистров с фиксированной шириной, обычно используемых в других архитектурах SIMD. Код Julia ничего не требует для подключения инструкций SVE/SVE2: векторизуемый код всегда использует инструкции SIMD, когда это возможно, а с LLVM 14 регистры SVE будут использоваться более агрессивно на процессорах, которые его поддерживают, таких как Fujitsu A64FX, Nvidia Grace, или серия ARM Neoverse. Обзор возможностей автовекторизации Julia SVE мы давали в вебинаре Julia на A64FX.

Арифметика с плавающей точкой на половинной точности

Gabriel Baraldi, Mosè Giordano

Для выполнения арифметических операций со значениями Float16 Julia переводила их в тип Float32, а затем преобразовывала обратно в Float16. В Julia 1.9 добавлена полноценная поддержка операций с типом Float16 на процессорах архитектуры AArch64, которые имеют аппаратную реализацию арифметики с плавающей точкой половинной точности. Например процессоры серии M от Apple или A64FX от Fujitsu. В приложениях, привязанных к памяти, это позволяет увеличить скорость до 2 раз по сравнению с операциями Float32 и в 4 раза по сравнению с операциями Float64. Например, эти функции доступны на MacBook c процесорами M1 и M2.

julia> using BenchmarkTools

julia> function sumsimd(v)
           sum = zero(eltype(v))
           @simd for x in v
               sum += x
           end
           return sum
       end
sumsimd (generic function with 1 method)

julia> @btime sumsimd(x) setup=(x=randn(Float16, 1_000_000))
  58.416 μs (0 allocations: 0 bytes)
Float16(551.0)

julia> @btime sumsimd(x) setup=(x=randn(Float32, 1_000_000))
  116.916 μs (0 allocations: 0 bytes)
897.7202f0

julia> @btime sumsimd(x) setup=(x=randn(Float64, 1_000_000))
  234.125 μs (0 allocations: 0 bytes)
1164.2247860232349

Милан Клёвер представил пример использования половинной точности для моделирования мелководья на A64FX в своем выступлении на JuliaCon 2021, "Ускорение A64FX в 3,6 раза путем сжатия ShallowWaters.jl в Float16".

Комментарии (0)