Часто задаваемые вопросы
Общие
Язык 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, если они не включены в документацию, даже если они имеют docstrings.
Есть полезная недокументированная функция/тип/константа. Могу ли я ее использовать?
Обновление 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.
Как передать параметры julia с помощью #!/usr/bin/env?
Передача параметров julia в так называемом shebang, например, #!/usr/bin/env julia --startup-file=no может не работать на некоторых платформах, таких как Linux. Это связано с тем, что обработка аргументов в shebang зависит от платформы и не имеет четкого определения. В среде Unix-подобных систем надёжный способ передачи параметров julia в исполняемом скрипте заключается в запуске скрипта как bash скрипта и использовании exec для замены процесса на julia:
#!/bin/bash
#=
exec julia --color=yes --startup-file=no "${BASH_SOURCE[0]}" "$@"
=#
@show ARGS # put any Julia code here
В приведённом выше примере код между #= и =# выполняется как bash скрипт. Julia игнорирует эту часть, так как это многострочный комментарий для Julia. Код Julia после =# игнорируется bash , так как он прекращает разбор файла, достигнув оператора exec.
Для того, чтобы перехватить CTRL-C в скрипте, можно использовать
#!/bin/bash
#=
exec julia --color=yes --startup-file=no -e 'include(popfirst!(ARGS))' \
"${BASH_SOURCE[0]}" "$@"
=#
@show ARGS # put any Julia code here
вместо этого. Обратите внимание, что с этой стратегией PROGRAM_FILE не будет установлена.
Функции
Я передал аргумент 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 внутри функции. Если вы хотите импортировать модуль, но использовать его символы только внутри определенной функции или набора функций, у вас есть два варианта:
-
Используйте
import:import Foo function bar(...) # ... refer to Foo symbols via Foo.baz ... endЭто загружает модуль
Fooи определяет переменнуюFoo, которая ссылается на модуль, но не импортирует какие-либо другие символы из модуля в текущее пространство имен. Вы обращаетесь к символамFooпо их полным именамFoo.barи т. д. -
Оберните свою функцию в модуль:
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 использовала больше ASCII-символов, оператор собирания можно было бы записать как <-... вместо ....
... разбивает один аргумент на множество различных аргументов в вызовах функций
В отличие от использования оператора ... для обозначения сбора многих различных аргументов в один аргумент при определении функции, оператор ... также используется для разделения одного аргумента функции на множество различных аргументов при использовании в контексте вызова функции. Это использование ... называется разбиением:
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 использовала больше ASCII-символов, оператор разбиения можно было бы записать как ...-> вместо ....
Какое значение возвращает присвоение?
Оператор = всегда возвращает правую часть, поэтому:
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}) at none:1
Это связано с тем, что Vector{Real} не является супертипом Vector{Int}! Вы можете решить эту проблему, используя что-то вроде foo(bar::Vector{T}) where {T<:Real} (или короткую форму foo(bar::Vector{<:Real}) если статический параметр T не нужен в теле функции). T — это универсальный символ: вы сначала указываете, что это должен быть подтип Real, а затем указываете, что функция принимает вектор с элементами этого типа.
Та же проблема возникает для любого составного типа 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, выделяется новый массив для хранения результата.
Хотя это поведение может удивить, выбор сделан осознанно. Основная причина заключается в наличии неизменяемых объектов в 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, причина расхождения, скорее всего, заключается в
библиотеке BLAS, используемой каждым языком,
количестве одновременных потоков.
Julia компилирует и использует собственную копию OpenBLAS, при этом количество потоков в настоящее время ограничено 8 (или количеством ваших ядер).
Изменение настроек OpenBLAS или компиляция Julia с другой библиотекой BLAS, например, Intel MKL, может улучшить производительность. Вы можете использовать MKL.jl, пакет, который заставляет Julia использовать для линейной алгебры Intel MKL BLAS и LAPACK вместо OpenBLAS, или найти предложения в форуме обсуждений о том, как настроить это вручную. Обратите внимание, что Intel MKL не может быть включён в Julia, так как он не является открытым исходным кодом.
Вычислительный кластер
Как управлять кэшами предварительной компиляции в распределённых файловых системах?
При использовании julia в мощных вычислительных установках (HPC), одновременное вызов n julia процессов создаёт не более n временных копий файлов кэша предварительной компиляции. Если это проблема (медленная и/или небольшая распределённая файловая система), вы можете:
- Использовать
juliaс флагом--compiled-modules=no, чтобы отключить предварительную компиляцию. - Настроить частный каталог записи с помощью
pushfirst!(DEPOT_PATH, private_path), гдеprivate_path— путь, уникальный для этогоjuliaпроцесса. Это также можно сделать, установив переменную средыJULIA_DEPOT_PATHв значение$private_path:$HOME/.julia. - Создать символическую ссылку от
~/.julia/compiledк каталогу в области временных файлов.
Выпуски Julia
Какую версию Julia использовать: Stable, LTS или nightly?
Версия Julia Stable — это последняя выпущенная версия Julia, которую большинство пользователей захотят запустить. Она имеет самые последние функции, включая улучшенную производительность. Версия Julia Stable имеет версионирование по SemVer как v1.x.y. Новый мажорный выпуск Julia, соответствующий новой версии Stable, создаётся примерно каждые 4-5 месяцев после нескольких недель тестирования в качестве кандидата в релиз. В отличие от версии LTS, версия Stable обычно не будет получать исправления ошибок после выпуска другой версии Stable Julia. Однако обновление до следующего стабильного релиза всегда будет возможным, так как каждый релиз Julia v1.x будет продолжать работать с кодом, написанным для более ранних версий.
Вы можете предпочесть LTS (Long Term Support) версию Julia, если вам нужна очень стабильная среда кода. Текущая версия LTS Julia имеет версионирование по SemVer как v1.0.x; эта ветка будет продолжать получать исправления ошибок до тех пор, пока не будет выбрана новая ветка LTS, после чего серия v1.0.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–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/faq/