Spec-Zone.ru › Julia 1.9

Часто задаваемые вопросы

Общие

Является ли Julia именем кого-либо или чего-либо?

Нет.

Почему вы не компилируете код Matlab/Python/R/… в Julia?

Поскольку многие люди знакомы с синтаксисом других динамических языков, и много кода уже написано на этих языках, естественно задаться вопросом, почему мы просто не подключили фронтенд Matlab или Python к бэкенду Julia (или не «транслировали» код в Julia), чтобы получить все преимущества производительности Julia, не требуя от программистов изучения нового языка. Просто, не так ли?

Основная проблема в том, что компилятор Julia ничем не отличается: мы используем обычный компилятор (LLVM) без какой-либо «секретной приправы», о которой другие разработчики языков не знают. Более того, компилятор Julia во многих отношениях намного проще, чем у других динамических языков (например, PyPy или LuaJIT). Преимущество Julia в производительности почти полностью связано с его фронтендом: семантика языка позволяет хорошо написанной программе на Julia предоставить компилятору больше возможностей для генерации эффективного кода и структуры памяти. Если вы попытаетесь скомпилировать код Matlab или Python в Julia, наш компилятор будет ограничен семантикой Matlab или Python, что приведет к созданию кода не лучше, чем у существующих компиляторов для этих языков (и, возможно, хуже). Ключевая роль семантики также объясняет, почему несколько существующих компиляторов Python (например, Numba и Pythran) пытаются оптимизировать лишь небольшой подмножество языка (например, операции над массивами Numpy и скалярами), и для этого подмножества они уже достигают, по крайней мере, такого же результата, как и мы для той же семантики. Люди, работающие над этими проектами, невероятно умны и достигли удивительных результатов, но перенос компилятора на язык, предназначенный для интерпретации, является очень сложной задачей.

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

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

С другой стороны, взаимодействие языков чрезвычайно полезно: мы хотим использовать существующий высококачественный код на других языках из Julia (и наоборот)! Лучший способ для этого — не транслятор, а удобные средства вызова между языками. Мы упорно работали над этим, от встроенного ccall (для вызова библиотек C и Fortran) до пакетов JuliaInterop, которые соединяют Julia с Python, Matlab, C++ и многим другим.

Открытый API

Как Julia определяет свой открытый API?

Единственные интерфейсы, стабильные относительно SemVer версии julia — это интерфейсы библиотек Julia Base и стандартных библиотек, описанные в документации и не помеченные как нестабильные (например, экспериментальные и внутренние). Функции, типы и константы не являются частью открытого API, если они не включены в документацию, даже если у них есть строка документации.

Есть полезная недокументированная функция/тип/константа. Могу ли я её использовать?

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

Документация недостаточно точна. Могу ли я полагаться на существующее поведение?

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

Сессии и REPL

Как удалить объект в памяти?

Julia не имеет аналога функции MATLAB clear; после определения имени в сессии Julia (технически, в модуле Main), оно всегда присутствует.

Если вас беспокоит использование памяти, вы всегда можете заменить объекты на объекты, потребляющие меньше памяти. Например, если A — массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = nothing. Память будет освобождена при следующем запуске сборщика мусора; вы можете заставить это произойти с помощью GC.gc(). Более того, попытка использовать A, вероятно, приведет к ошибке, поскольку большинство методов не определены для типа Nothing.

Как изменить объявление типа в моей сессии?

Возможно, вы определили тип, а затем поняли, что вам нужно добавить новое поле. Если вы попытаетесь сделать это в REPL, получите ошибку:

ERROR: invalid redefinition of constant MyType

Типы в модуле Main нельзя переопределить.

Хотя это может быть неудобно при разработке нового кода, есть отличное решение. Модули можно заменить, переопределив их, и поэтому, если вы обернёте весь свой новый код в модуль, вы можете переопределить типы и константы. Вы не можете импортировать имена типов в Main и ожидать возможности переопределить их там, но вы можете использовать имя модуля для разрешения области видимости. Другими словами, при разработке вы можете использовать что-то вроде такого рабочего процесса:

include("mynewcode.jl")              # this defines a module MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Got an error. Change something in "mynewcode.jl"
include("mynewcode.jl")              # reload the module
obj1 = MyModule.ObjConstructor(a, b) # old objects are no longer valid, must reconstruct
obj2 = MyModule.somefunction(obj1)   # this time it worked!
obj3 = MyModule.someotherfunction(obj2, c)
...

Сценарии

Как проверить, запускается ли текущий файл как основной сценарий?

При запуске файла в качестве основного сценария с помощью julia file.jl можно активировать дополнительную функциональность, такую как обработка аргументов командной строки. Способ определения того, что файл запущен таким образом, — это проверка того, является ли abspath(PROGRAM_FILE) == @__FILE__ true.

Как перехватить CTRL-C в скрипте?

Запуск скрипта Julia с помощью julia file.jl не генерирует InterruptException при попытке его завершить с помощью CTRL-C (SIGINT). Для выполнения определённого кода перед завершением скрипта Julia, который может быть вызван или не вызван CTRL-C, используйте atexit. В качестве альтернативы, вы можете использовать julia -e 'include(popfirst!(ARGS))' file.jl для выполнения скрипта, одновременно перехватывая InterruptException в блоке try. Обратите внимание, что при этом стратегии PROGRAM_FILE не будет установлено.

Как передать параметры julia с помощью #!/usr/bin/env?

Передача параметров julia в строке shebang, как в #!/usr/bin/env julia --startup-file=no, не будет работать на многих платформах (BSD, macOS, Linux), где ядро, в отличие от оболочки, не разделяет аргументы по пробелам. Опция env -S, которая разделяет одну строку аргументов на несколько аргументов по пробелам, аналогично оболочке, предлагает простое решение:

#!/usr/bin/env -S julia --color=yes --startup-file=no
@show ARGS  # put any Julia code here

Опция env -S появилась в FreeBSD 6.0 (2005), macOS Sierra (2016) и GNU/Linux coreutils 8.30 (2018).

Почему run не поддерживает * или конвейеры для сценариев внешних программ?

Функция Julia run запускает внешние программы непосредственно, не вызывая операционную оболочку (в отличие от функции system("...") в других языках, таких как Python, R или C). Это означает, что run не выполняет расширение подстановок * («глоббинга»), а также не интерпретирует конвейеры оболочки, такие как | или >.

Однако вы по-прежнему можете использовать подстановки и конвейеры с помощью функций Julia. Например, встроенная функция pipeline позволяет объединять внешние программы и файлы, аналогично конвейерам оболочки, а пакет Glob.jl реализует подстановки, совместимые с POSIX.

END_OF_DOCUMENT_MARKER ```

Конечно, вы можете запускать программы через оболочку, явно передавая оболочку и строку команды в run, например, run(`sh -c "ls > files.txt"`) для использования Unix-оболочки Bourne shell, но в целом следует отдавать предпочтение чистому скриптингу на Julia, как run(pipeline(`ls`, "files.txt")). Причина, по которой мы по умолчанию избегаем оболочки, заключается в том, что вызов оболочки плох: запуск процессов через оболочку медленный, уязвим к цитированию специальных символов, имеет слабую обработку ошибок и проблематичен для переносимости. (Разработчики Python пришли к подобному выводу.)

Переменные и присваивания

Почему я получаю UndefVarError из простого цикла?

Возможно, у вас что-то вроде:

x = 0
while x < 10
    x += 1
end

и вы заметили, что это работает нормально в интерактивной среде (например, в Julia REPL), но дает UndefVarError: `x` not defined при попытке выполнить его в скрипте или другом файле. Проблема в том, что Julia, как правило, требует явного присваивания глобальных переменных в локальной области.

Здесь x — глобальная переменная, while определяет локальную область, а x += 1 — присваивание глобальной переменной в этой локальной области.

Как упоминалось выше, Julia (версия 1.5 или более поздняя) позволяет вам опустить ключевое слово global для кода в REPL (и во многих других интерактивных средах), чтобы упростить изучение (например, копирование и вставка кода из функции для интерактивного выполнения). Однако при работе с кодом в файлах Julia требует более дисциплинированного подхода к глобальным переменным. У вас есть по крайней мере три варианта:

  1. Поместите код в функцию (так что x — локальная переменная в функции). В целом, хорошее проектирование ПО предполагает использование функций вместо глобальных скриптов (поищите в интернете «почему глобальные переменные плохи», чтобы увидеть много объяснений). В Julia глобальные переменные также медленные.
  2. Оборатите код в блок let. (Это делает x локальной переменной внутри оператора let ... end, снова устраняя необходимость в global).
  3. Явно отметьте x как global внутри локальной области перед присваиванием ему, например, напишите global x += 1.

Более подробные объяснения можно найти в разделе руководства о мягкой области видимости.

Функции

Я передал аргумент x в функцию, изменил его внутри функции, но снаружи переменная x по-прежнему не изменилась. Почему?

Предположим, вы вызываете функцию так:

julia> x = 10
10

julia> function change_value!(y)
           y = 17
       end
change_value! (generic function with 1 method)

julia> change_value!(x)
17

julia> x # x is unchanged!
10

В Julia привязка переменной x не может быть изменена путем передачи x в качестве аргумента функции. При вызове change_value!(x) в приведенном выше примере y — это новая переменная, первоначально привязанная к значению x, т. е. 10; затем y перепривязывается к константе 17, в то время как переменная x внешней области остается неизменной.

Однако, если x привязана к объекту типа Array (или любого другого изменяемого типа). Изнутри функции вы не можете «отвязать» x от этого массива, но вы можете изменить его содержимое. Например:

julia> x = [1,2,3]
3-element Vector{Int64}:
 1
 2
 3

julia> function change_array!(A)
           A[1] = 5
       end
change_array! (generic function with 1 method)

julia> change_array!(x)
5

julia> x
3-element Vector{Int64}:
 5
 2
 3

Здесь мы создали функцию change_array!, которая присваивает 5 первому элементу переданного массива (привязанного к x в месте вызова и привязанного к A внутри функции). Обратите внимание, что после вызова функции x по-прежнему привязана к тому же массиву, но содержимое этого массива изменилось: переменные A и x были различными связями, ссылающимися на один и тот же изменяемый объект Array.

Можно ли использовать using или import внутри функции?

Нет, у вас не должно быть оператора using или import внутри функции. Если вы хотите импортировать модуль, но использовать его символы только внутри определенной функции или набора функций, у вас есть два варианта:

  1. Используйте import:

    import Foo
    function bar(...)
        # ... refer to Foo symbols via Foo.baz ...
    end

    Это загружает модуль Foo и определяет переменную Foo , которая ссылается на модуль, но не импортирует другие символы из модуля в текущее пространство имен. Вы ссылаетесь на символы Foo с помощью их полных имен Foo.bar и т. д.

  2. Оборатите свою функцию в модуль:

    module Bar
    export bar
    using Foo
    function bar(...)
        # ... refer to Foo.baz as simply baz ....
    end
    end
    using Bar

    Это импортирует все символы из Foo, но только внутри модуля Bar.

Что делает оператор ...?

Два использования оператора ...: подбор и разбиение

Многие новые пользователи Julia находят использование оператора ... запутанным. Часть того, что делает оператор ... запутанным, заключается в том, что он означает две разные вещи в зависимости от контекста.

... объединяет несколько аргументов в один аргумент в определениях функций

В контексте определений функций оператор ... используется для объединения нескольких аргументов в один аргумент. Это использование оператора ... для объединения нескольких аргументов в один называется подбором:

julia> function printargs(args...)
           println(typeof(args))
           for (i, arg) in enumerate(args)
               println("Arg #$i = $arg")
           end
       end
printargs (generic function with 1 method)

julia> printargs(1, 2, 3)
Tuple{Int64, Int64, Int64}
Arg #1 = 1
Arg #2 = 2
Arg #3 = 3

Если бы Julia использовала больше латинских символов, оператор подбора, возможно, был бы написан как <-... вместо ....

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

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

julia> function threeargs(a, b, c)
           println("a = $a::$(typeof(a))")
           println("b = $b::$(typeof(b))")
           println("c = $c::$(typeof(c))")
       end
threeargs (generic function with 1 method)

julia> x = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> threeargs(x...)
a = 1::Int64
b = 2::Int64
c = 3::Int64

Если бы Julia использовала больше латинских символов, оператор разбиения, возможно, был бы написан как ...-> вместо ....

Какое значение возвращает операция присваивания?

Оператор = всегда возвращает правую часть, поэтому:

julia> function threeint()
           x::Int = 3.0
           x # returns variable x
       end
threeint (generic function with 1 method)

julia> function threefloat()
           x::Int = 3.0 # returns 3.0
       end
threefloat (generic function with 1 method)

julia> threeint()
3

julia> threefloat()
3.0

и аналогично:

julia> function twothreetup()
           x, y = [2, 3] # assigns 2 to x and 3 to y
           x, y # returns a tuple
       end
twothreetup (generic function with 1 method)

julia> function twothreearr()
           x, y = [2, 3] # returns an array
       end
twothreearr (generic function with 1 method)

julia> twothreetup()
(2, 3)

julia> twothreearr()
2-element Vector{Int64}:
 2
 3

Типы, объявления типов и конструкторы

Что означает «стабильность типа»?

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

julia> function unstable(flag::Bool)
           if flag
               return 1
           else
               return 1.0
           end
       end
unstable (generic function with 1 method)

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

Почему Julia возвращает DomainError для определённых, на первый взгляд, осмысленных операций?

Определённые операции математически осмысленны, но приводят к ошибкам:

julia> sqrt(-2.0)
ERROR: DomainError with -2.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Это поведение — неудобное следствие требования стабильности типов. В случае sqrt, большинство пользователей хотят, чтобы sqrt(2.0) возвращало действительное число и расстроятся, если оно вернёт комплексное число 1.4142135623730951 + 0.0im. Можно написать функцию sqrt, чтобы переключаться на комплексные значения только при передаче отрицательного числа (что делает sqrt в некоторых других языках), но тогда результат не будет стабильным по типу, и функция sqrt будет иметь низкую производительность.

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

julia> sqrt(-2.0+0im)
0.0 + 1.4142135623730951im

Как я могу ограничить или вычислить параметры типа?

Параметры параметрического типа параметрического типа могут содержать либо типы, либо значения битов, и сам тип выбирает, как использовать эти параметры. Например, Array{Float64, 2} параметризован типом Float64 для выражения типа элемента и целым значением 2 для выражения числа измерений. При определении собственного параметрического типа вы можете использовать ограничения подтипов, чтобы объявить, что определённый параметр должен быть подтипом (<:) некоторого абстрактного типа или предыдущего параметра типа. Однако нет специальной синтаксической конструкции для объявления, что параметр должен быть значением заданного типа — то есть вы не можете напрямую объявить, что параметр, подобный параметру размерности, isa Int в определении struct, например. Аналогично, вы не можете выполнять вычисления (включая простые операции, такие как сложение или вычитание) над параметрами типа. Вместо этого такие ограничения и отношения могут быть выражены с помощью дополнительных параметров типа, которые вычисляются и проверяются в конструкторах типа конструкторов.

В качестве примера рассмотрим

struct ConstrainedType{T,N,N+1} # NOTE: INVALID SYNTAX
    A::Array{T,N}
    B::Array{T,N+1}
end

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

struct ConstrainedType{T,N,M}
    A::Array{T,N}
    B::Array{T,M}
    function ConstrainedType(A::Array{T,N}, B::Array{T,M}) where {T,N,M}
        N + 1 == M || throw(ArgumentError("second argument should have one more axis" ))
        new{T,N,M}(A, B)
    end
end

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

ConstrainedType(A) = ConstrainedType(A, compute_B(A))

Почему Julia использует машинную арифметику для целых чисел?

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

julia> x = typemax(Int)
9223372036854775807

julia> y = x+1
-9223372036854775808

julia> z = -y
-9223372036854775808

julia> 2*z
0

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

Одним из вариантов было бы проверять каждую операцию с целыми числами на переполнение и продвигать результаты к более крупным типам целых чисел, таким как Int128 или BigInt в случае переполнения. К сожалению, это вводит значительную нагрузку при каждой операции с целыми числами (например, при инкременте счётчика цикла) — требуется генерировать код для выполнения проверок переполнения во время выполнения после арифметических инструкций и переходов для обработки потенциальных переполнений. Хуже того, это приведет к тому, что каждое вычисление с целыми числами будет иметь неустойчивость типа. Как мы упоминали выше, устойчивость типа имеет важное значение для эффективной генерации эффективного кода. Если вы не можете рассчитывать на то, что результаты операций с целыми числами будут целыми числами, то невозможно сгенерировать быстрый, простой код, как это делают компиляторы C и Fortran.

Вариант этого подхода, который избегает видимой неустойчивости типа, заключается в объединении типов Int и BigInt в один гибридный тип целых чисел, который внутренне меняет представление, когда результат больше не помещается в размер машинного целого числа. Хотя это внешне избегает неустойчивости типа на уровне кода Julia, оно просто скрывает проблему, возлагая те же трудности на код C, реализующий этот гибридный тип целых чисел. Этот подход может сработать и даже может быть довольно быстрым во многих случаях, но имеет несколько недостатков. Одна проблема заключается в том, что представление целых чисел в памяти и массивов целых чисел больше не совпадает с естественным представлением, используемым языками C, Fortran и другими языками с машинными целыми числами. Таким образом, для взаимодействия с этими языками нам в конечном итоге всё равно придётся ввести типы целых чисел. Любое неограниченное представление целых чисел не может иметь фиксированное количество битов и, следовательно, не может храниться в строке массива с фиксированными слотами — большие целые значения всегда будут требовать отдельного хранилища в куче. И, конечно же, независимо от того, насколько умной реализацией гибридных целых чисел вы будете пользоваться, всегда существуют проблемные места — ситуации, когда производительность неожиданно ухудшается. Сложное представление, отсутствие возможности взаимодействия с C и Fortran, невозможность представления массивов целых чисел без дополнительного выделения памяти в куче и непредсказуемые характеристики производительности делают даже самые умные реализации гибридных целых чисел плохим выбором для высокопроизводительных численных вычислений.

Альтернативой использованию гибридных целых чисел или продвижению к BigInts является использование насыщающей арифметики целых чисел, где добавление к наибольшему целому значению оставляет его неизменным, а также для вычитания из наименьшего целого значения. Именно это делает Matlab™:

>> int64(9223372036854775807)

ans =

  9223372036854775807

>> int64(9223372036854775807) + 1

ans =

  9223372036854775807

>> int64(-9223372036854775808)

ans =

 -9223372036854775808

>> int64(-9223372036854775808) - 1

ans =

 -9223372036854775808

На первый взгляд, это кажется достаточно разумным, так как 9223372036854775807 намного ближе к 9223372036854775808, чем -9223372036854775808, и целые числа по-прежнему представлены фиксированным размером естественным образом, совместимым с C и Fortran. Однако насыщающая арифметика целых чисел глубоко проблематична. Первая и наиболее очевидная проблема заключается в том, что это не то, как работает машинная арифметика целых чисел, поэтому реализация насыщающих операций требует выполнения инструкций после каждой операции машинного целого числа для проверки на недополнение или переполнение и замены результата на typemin(Int) или typemax(Int) соответственно. Само по себе это расширяет каждую операцию с целыми числами с одной быстрой инструкции до полудюжины инструкций, вероятно, включая ветвления. Ой. Но дело в том, что насыщающая арифметика целых чисел не ассоциативна. Рассмотрим это вычисление в Matlab:

>> n = int64(2)^62
4611686018427387904

>> n + (n - 1)
9223372036854775807

>> (n + n) - 1
9223372036854775806

Это затрудняет написание многих основных алгоритмов для целых чисел, так как многие общие методы зависят от того, что машинная операция сложения с переполнением является ассоциативной. Рассмотрим нахождение середины между целыми значениями lo и hi в Julia с помощью выражения (lo + hi) >>> 1:

julia> n = 2^62
4611686018427387904

julia> (n + 2n) >>> 1
6917529027641081856

Видите? Нет проблем. Это правильная середина между 2^62 и 2^63, несмотря на то, что n + 2n равно -4611686018427387904. Теперь попробуйте это в Matlab:

>> (n + 2*n)/2

ans =

  4611686018427387904

Упс. Добавление оператора >>> в Matlab не помогло бы, потому что насыщение, которое происходит при сложении n и 2n, уже уничтожило информацию, необходимую для вычисления правильной середины.

Отсутствие ассоциативности не только неприятно для программистов, которые не могут полагаться на неё для методов, подобных этому, но и делает невозможным оптимизацию компиляторами арифметических операций с целыми числами. Например, поскольку целые числа Julia используют обычную машинную арифметику целых чисел, LLVM свободно оптимизирует простые маленькие функции, такие как f(k) = 5k-1. Машинный код этой функции таков:

julia> code_native(f, Tuple{Int})
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 1
  leaq  -1(%rdi,%rdi,4), %rax
  popq  %rbp
  retq
  nopl  (%rax,%rax)

Фактическое тело функции — это одна инструкция leaq, которая вычисляет умножение и сложение целых чисел одновременно. Это ещё более полезно, когда f подключается к другой функции:

julia> function g(k, n)
           for i = 1:n
               k = f(k)
           end
           return k
       end
g (generic function with 1 methods)

julia> code_native(g, Tuple{Int,Int})
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 2
  testq %rsi, %rsi
  jle L26
  nopl  (%rax)
Source line: 3
L16:
  leaq  -1(%rdi,%rdi,4), %rdi
Source line: 2
  decq  %rsi
  jne L16
Source line: 5
L26:
  movq  %rdi, %rax
  popq  %rbp
  retq
  nop

Так как вызов f подключается, тело цикла оказывается всего лишь одной инструкцией leaq . Теперь рассмотрим, что происходит, если мы сделаем количество итераций цикла постоянным:

julia> function g(k)
           for i = 1:10
               k = f(k)
           end
           return k
       end
g (generic function with 2 methods)

julia> code_native(g,(Int,))
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 3
  imulq $9765625, %rdi, %rax    # imm = 0x9502F9
  addq  $-2441406, %rax         # imm = 0xFFDABF42
Source line: 5
  popq  %rbp
  retq
  nopw  %cs:(%rax,%rax)

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

Самый разумный способ избежать того, чтобы арифметика целых чисел бесшумно переполнялась, — это использовать проверенную арифметику повсюду, генерируя ошибки при переполнении при сложении, вычитании и умножении, создавая значения, которые не являются верными по значению. В этом блоге Дан Луу анализирует это и обнаруживает, что вместо тривиальных затрат, которые, в теории, должен иметь этот подход, он оказывается достаточно дорогостоящим из-за того, что компиляторы (LLVM и GCC) не оптимизируют дополнительные проверки переполнения. Если в будущем это улучшится, мы можем рассмотреть возможность использования по умолчанию проверенной арифметики целых чисел в Julia, но пока мы должны смириться с возможностью переполнения.

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

Какие возможные причины ошибки UndefVarError во время удалённого выполнения?

Как указывает ошибка, непосредственной причиной ошибки UndefVarError на удалённом узле является отсутствие связи с таким именем. Давайте рассмотрим некоторые возможные причины.

julia> module Foo
           foo() = remotecall_fetch(x->x, 2, "Hello")
       end

julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `Foo` not defined
Stacktrace:
[...]

Закрытие x->x несёт ссылку на Foo, и так как Foo недоступно на узле 2, генерируется ошибка UndefVarError.

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

julia> @everywhere module Foo
           function foo()
               global gvar = "Hello"
               remotecall_fetch(()->gvar, 2)
           end
       end

julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `gvar` not defined
Stacktrace:
[...]

В приведённом примере @everywhere module Foo определил Foo на всех узлах. Однако вызов Foo.foo() создал новую глобальную привязку gvar на локальном узле, но она не была найдена на узле 2, что привело к ошибке UndefVarError.

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

julia> gvar_self = "Node1"
"Node1"

julia> remotecall_fetch(()->gvar_self, 2)
"Node1"

julia> remotecall_fetch(varinfo, 2)
name          size summary
––––––––– –––––––– –––––––
Base               Module
Core               Module
Main               Module
gvar_self 13 bytes String

Это не относится к объявлениям function или struct. Однако, анонимные функции, привязанные к глобальным переменным, сериализуются, как видно ниже.

julia> bar() = 1
bar (generic function with 1 method)

julia> remotecall_fetch(bar, 2)
ERROR: On worker 2:
UndefVarError: `#bar` not defined
[...]

julia> anon_bar  = ()->1
(::#21) (generic function with 1 method)

julia> remotecall_fetch(anon_bar, 2)
1

Устранение неполадок "метод не соответствует": параметрическая инвариантность типов и MethodError

Почему не работает объявление foo(bar::Vector{Real}) = 42 и затем вызов foo([1])?

Как вы увидите, если попробуете это, результатом будет MethodError:

julia> foo(x::Vector{Real}) = 42
foo (generic function with 1 method)

julia> foo([1])
ERROR: MethodError: no method matching foo(::Vector{Int64})

Closest candidates are:
  foo(!Matched::Vector{Real})
   @ Main none:1

Stacktrace:
[...]

Это происходит потому, что Vector{Real} не является супертипом Vector{Int}! Вы можете решить эту проблему с помощью чего-то вроде foo(bar::Vector{T}) where {T<:Real} (или сокращённой формы foo(bar::Vector{<:Real}) если статический параметр T не нужен в теле функции). T — это универсальный маркер: сначала указывается, что он должен быть подтипом Real, а затем, что функция принимает Vector с элементами этого типа.

Та же проблема возникает для любого составного типа Comp, а не только для Vector Если Comp имеет параметр, объявленный как тип Y, то другой тип Comp2 с параметром типа X<:Y не является подтипом Comp Это инвариантность типа (в отличие от Tuple, которая является ковариантной по своим параметрам). Для более подробного объяснения этих вопросов см. Параметрические составные типы.

Почему Julia использует * для конкатенации строк? Почему не + или что-то ещё?

Главный аргумент против + состоит в том, что конкатенация строк не коммутативна, тогда как + обычно используется как коммутативный оператор. Хотя сообщество Julia признаёт, что другие языки используют разные операторы и * могут быть непривычны для некоторых пользователей, это позволяет передавать определённые алгебраические свойства.

Обратите внимание, что вы также можете использовать string(...) для конкатенации строк (и других значений, преобразованных в строки); аналогично, repeat может быть использовано вместо ^ для повторения строк. Также полезна синтаксис интерполяции для построения строк.

Пакеты и модули

В чём разница между "using" и "import"?

Существует только одно различие, и на первый взгляд (с точки зрения синтаксиса) оно может показаться очень незначительным. Разница между using и import в том, что с using вам нужно указать function Foo.bar(.. для расширения функции bar модуля Foo новым методом, а с import Foo.bar, вам достаточно указать function bar(... и он автоматически расширит функцию bar модуля Foo.

Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширять функцию, о существовании которой вы не знали, потому что это может легко привести к ошибке. Это наиболее вероятно произойдёт с методом, принимающим общий тип, такой как строка или целое число, поскольку как вы, так и другой модуль могли бы определить метод для обработки такого общего типа. Если вы используете import, то вы замените реализацию bar(s::AbstractString) другого модуля своей новой реализацией, которая легко может делать что-то совершенно другое (и сломать все/многие будущие использования других функций в модуле Foo, которые зависят от вызова bar).

Ничто и пропущенные значения

Как работают "null", "ничто" или "пропущенное значение" в Julia?

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

Некоторые функции используются только для побочных эффектов и не нуждаются в возвращаемом значении. В таких случаях принято возвращать значение nothing, которое представляет собой просто одиночный объект типа Nothing. Это обычный тип без полей; в нём нет ничего особенного, кроме этой конвенции и того, что REPL ничего не выводит для него. Некоторые языковые конструкции, которые иначе не имели бы значения, также возвращают nothing, например if false; end.

Для ситуаций, когда значение x типа T существует только иногда, тип Union{T, Nothing} может использоваться для аргументов функций, полей объектов и типов элементов массивов, как эквивалент Nullable, Option или Maybe в других языках. Если само значение может быть nothing (особенно, когда T равно Any ), более подходящим является тип Union{Some{T}, Nothing} , поскольку x == nothing обозначает отсутствие значения, а x == Some(nothing) обозначает наличие значения, равного nothing Функция something позволяет распаковывать объекты Some и использовать значение по умолчанию вместо аргументов nothing Компилятор способен генерировать эффективный код при работе с аргументами или полями типа Union{T, Nothing}.

Для представления отсутствующих данных в статистическом смысле (NA в R или NULL в SQL) используйте объект missing. Для более подробной информации см. раздел Missing Values.

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

Пустой (или «нижний») тип, записанный как Union{} (пустой тип объединения), — это тип без значений и без подтипов (кроме самого себя). Вам, как правило, не придётся использовать этот тип.

Память

Почему x += y выделяет память, когда x и y — массивы?

В Julia, x += y заменяется во время понижения на x = x + y. Для массивов это означает, что вместо хранения результата в той же области памяти, что и x, создаётся новый массив для хранения результата. Если вы хотите изменить x, используйте x .+= y для обновления каждого элемента индивидуально.

Хотя это поведение может удивить, выбор сделан осознанно. Основная причина — наличие неизменяемых объектов в Julia, которые не могут изменить своё значение после создания. Действительно, число — неизменяемый объект; операторы x = 5; x += 1 не изменяют значение 5, а изменяют значение, привязанное к x Для неизменяемого объекта единственный способ изменить значение — переназначить его.

Чтобы немного расширить, рассмотрим следующую функцию:

function power_by_squaring(x, n::Int)
    ispow2(n) || error("This implementation only works for powers of 2")
    while n >= 2
        x *= x
        n >>= 1
    end
    x
end

После вызова, такого как x = 5; y = power_by_squaring(x, 4), вы получите ожидаемый результат: x == 5 && y == 625. Однако теперь предположим, что *=, при использовании с матрицами, вместо этого изменяет левую часть. Возникнут две проблемы:

  • Для общих квадратных матриц, A = A*B не может быть реализована без временного хранения: A[1,1] вычисляется и хранится в левой части до завершения использования в правой части.
  • Предположим, вы готовы выделить временную переменную для вычисления (что лишит большей части смысла сделать *= локальной); если вы воспользуетесь изменяемостью x, эта функция будет вести себя по-разному для изменяемых и неизменяемых входных данных. В частности, для неизменяемых x после вызова у вас (в общем случае) будет y != x, но для изменяемых x у вас будет y == x.

Поскольку поддержка обобщённого программирования считается более важной, чем потенциальные оптимизации производительности, которые могут быть достигнуты другими средствами (например, с помощью вещания или явных циклов), операторы, такие как += и *= работают, перепривязывая новые значения.

Асинхронный ввод-вывод и одновременные синхронные записи

Почему одновременные записи в один поток приводят к перемешанному выводу?

Хотя API потокового ввода-вывода является синхронным, его реализация полностью асинхронна.

Рассмотрим вывод, полученный из следующего:

julia> @sync for i in 1:3
           @async write(stdout, string(i), " Foo ", " Bar ")
       end
123 Foo  Foo  Foo  Bar  Bar  Bar

Это происходит потому, что, хотя вызов write синхронный, запись каждого аргумента переключается на другие задачи, ожидая завершения части ввода-вывода.

print и println «запирают» поток во время вызова. Следовательно, изменение write на println в приведённом выше примере приведёт к:

julia> @sync for i in 1:3
           @async println(stdout, string(i), " Foo ", " Bar ")
       end
1 Foo  Bar
2 Foo  Bar
3 Foo  Bar

Вы можете заблокировать записи с помощью ReentrantLock следующим образом:

julia> l = ReentrantLock();

julia> @sync for i in 1:3
           @async begin
               lock(l)
               try
                   write(stdout, string(i), " Foo ", " Bar ")
               finally
                   unlock(l)
               end
           end
       end
1 Foo  Bar 2 Foo  Bar 3 Foo  Bar

Массивы

Какие различия между массивами нулевой размерности и скалярами?

Массивы нулевой размерности — это массивы вида Array{T,0}. Они ведут себя подобно скалярам, но существуют важные различия. Они заслуживают особого упоминания, так как являются особым случаем, логически вытекающим из общего определения массивов, но могут быть несколько неинтуитивными с первого взгляда. Следующая строка определяет массив нулевой размерности:

julia> A = zeros()
0-dimensional Array{Float64,0}:
0.0

В этом примере A — это изменяемый контейнер, содержащий один элемент, который можно установить с помощью A[] = 1.0 и получить с помощью A[]. Все массивы нулевой размерности имеют одинаковый размер (size(A) == ()) и длину (length(A) == 1). В частности, массивы нулевой размерности не пустые. Если это кажется неинтуитивным, вот несколько идей, которые могут помочь понять определение массивов в Julia.

  • Массивы нулевой размерности — это «точка» к «линии» вектора и «плоскости» матрицы. Так же как линия не имеет площади (но все же представляет собой набор вещей), точка не имеет длины или каких-либо измерений (но все же представляет собой вещь).
  • Мы определим prod(()) как 1, а общее количество элементов в массиве — это произведение размеров. Размер массива нулевой размерности — (), а следовательно, его длина — 1.
  • Массивы нулевой размерности не имеют собственных измерений для индексирования — они просто A[]. К ним можно применить то же правило «приводящего к единице» для всех других размерностей массивов, поэтому их можно индексировать как A[1], A[1,1], и так далее; см. Пропущенные и дополнительные индексы.

Также важно понимать отличия от обычных скаляров. Скаляры не являются изменяемыми контейнерами (хотя они итерируемы и определяют такие вещи, как length, getindex, например 1[] == 1). В частности, если x = 0.0 определён как скаляр, попытка изменить его значение с помощью x[] = 1.0 является ошибкой. Скаляр x может быть преобразован в массив нулевой размерности, содержащий его, с помощью fill(x), и, наоборот, массив нулевой размерности a может быть преобразован в содержащийся в нём скаляр с помощью a[]. Другое различие заключается в том, что скаляр может участвовать в линейно-алгебраических операциях, таких как 2 * rand(2,2), но аналогичная операция с массивом нулевой размерности fill(2) * rand(2,2) является ошибкой.

Почему результаты моих тестов производительности Julia для линейно-алгебраических операций отличаются от других языков?

Вы можете обнаружить, что простые тесты производительности линейно-алгебраических блоков, таких как

using BenchmarkTools
A = randn(1000, 1000)
B = randn(1000, 1000)
@btime $A \ $B
@btime $A * $B

могут отличаться при сравнении с другими языками, такими как Matlab или R.

Поскольку такие операции являются очень тонкими оболочками над соответствующими функциями BLAS, причиной различий, скорее всего, является

  1. библиотека BLAS, используемая каждым языком,

  2. количество параллельных потоков.

Julia компилирует и использует свою собственную копию OpenBLAS, с потоками, ограниченными в настоящее время 8 (или количеством ваших ядер).

Изменение настроек OpenBLAS или компиляция Julia с другой библиотекой BLAS, например Intel MKL, может улучшить производительность. Вы можете использовать MKL.jl, пакет, который заставляет Julia использовать Intel MKL BLAS и LAPACK вместо OpenBLAS, или найти рекомендации в форуме обсуждений о том, как это настроить вручную. Обратите внимание, что Intel MKL не может быть включен в Julia, так как он не является открытым исходным кодом.

Вычислительный кластер

Как управлять кэшами предварительной компиляции в распределённых файловых системах?

При использовании julia в высокопроизводительных вычислительных (ВВС) установках вызов n julia процессов одновременно создаёт не более n временных копий файлов кэша предварительной компиляции. Если это проблема (медленная и/или небольшая распределённая файловая система), вы можете:

  1. Использовать julia с флагом --compiled-modules=no для отключения предварительной компиляции.
  2. Настроить личный каталог записи с помощью pushfirst!(DEPOT_PATH, private_path), где private_path — это путь, уникальный для данного процесса julia. Это также можно сделать, задав переменную окружения JULIA_DEPOT_PATH значение $private_path:$HOME/.julia.
  3. Создать символическую ссылку от ~/.julia/compiled до каталога в пространстве временного хранения.

Выпуски Julia

Хотите ли вы использовать стабильную, LTS или ночную версию Julia?

Стабильная версия Julia — это последняя выпущенная версия Julia, которую большинство пользователей захотят использовать. Она имеет самые последние функции, включая улучшенную производительность. Стабильная версия Julia имеет версию, обозначенную по SemVer как v1.x.y. Новый мажорный выпуск Julia, соответствующий новой стабильной версии, выпускается примерно каждые 4-5 месяцев после нескольких недель тестирования в качестве кандидата на выпуск. В отличие от LTS-версии, стабильная версия обычно не получает исправления ошибок после выпуска другой стабильной версии Julia. Однако переход к следующему стабильному выпуску всегда возможен, так как каждый выпуск Julia v1.x будет продолжать работать с кодом, написанным для более ранних версий.

Вы можете предпочесть LTS (Long Term Support) версию Julia, если вам нужна очень стабильная база кода. Текущая LTS-версия Julia имеет версию, обозначенную по SemVer как v1.6.x; этот ветвь будет продолжать получать исправления ошибок до выбора новой LTS-ветви, после чего серия v1.6.x больше не будет получать регулярных исправлений ошибок, и всем, кроме самых консервативных пользователей, будет рекомендовано перейти на новую серию LTS-версий. Как разработчик пакетов, вы можете предпочесть разрабатывать для LTS-версии, чтобы максимизировать количество пользователей, которые могут использовать ваш пакет. Согласно SemVer, код, написанный для v1.0, будет продолжать работать для всех будущих LTS и стабильных версий. Как правило, даже если вы ориентируетесь на LTS, вы можете разрабатывать и запускать код в последней стабильной версии, чтобы воспользоваться улучшенной производительностью; при условии, что вы избегаете использования новых функций (таких как добавленные функции библиотек или новые методы).

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

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

Ссылки на каждый из этих типов загрузок можно найти на странице загрузки по адресу https://julialang.org/downloads/. Обратите внимание, что не все версии Julia доступны для всех платформ.

Как перенести список установленных пакетов после обновления моей версии Julia?

Каждая мажорная версия Julia имеет свою собственную стандартную среду. В результате, при установке новой мажорной версии Julia пакеты, добавленные вами с использованием предыдущей мажорной версии, по умолчанию недоступны. Среда для данной версии Julia определяется файлами Project.toml и Manifest.toml в папке, соответствующей номеру версии в .julia/environments/, например, .julia/environments/v1.3.

Если вы установили новую мажорную версию Julia, скажем, 1.4, и хотите использовать в её стандартной среде те же пакеты, что и в предыдущей версии (например, 1.3), вы можете скопировать содержимое файла Project.toml из папки 1.3 в 1.4 . Затем, в сеансе новой версии Julia, перейдите в режим управления пакетами, набрав клавишу ], и выполните команду instantiate.

Эта операция найдёт множество подходящих пакетов из скопированного файла, совместимых с целевой версией Julia, и установит или обновит их, если возможно. Если вы хотите воспроизвести не только набор пакетов, но и версии, которые вы использовали в предыдущей версии Julia, вам также следует скопировать файл Manifest.toml перед запуском команды Pkg instantiate. Однако обратите внимание, что пакеты могут определять ограничения совместимости, которые могут измениться при изменении версии Julia, поэтому точный набор версий, который у вас был в 1.3, может не работать для 1.4.

© 2009–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.9/manual/faq/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API