https://docs.julialang.org/en/latest/manual/faq/?highlight=Mutable#Frequently-Asked-Questions-1
Нет
Поскольку многие люди знакомы с синтаксисом других динамических языков, и множество кода уже было написано на этих языках, естественно задаться вопросом, почему мы просто не подключили интерфейс Matlab или Python к интерфейсу Julia (или не “транспилировали” код в Julia), чтобы получить все преимущества производительности Julia, не требуя от программистов изучения нового языка. Просто, правда?
Основная проблема заключается в том, что в компиляторе Julia нет ничего особенного: мы используем обычный компилятор (LLVM) без какого-либо “секретного соуса”, о котором не знают другие разработчики. Действительно, компилятор Julia во многих отношениях намного проще, чем у других динамических языков (например, PyPy или LuaJIT). Преимущество Джулии в производительности почти полностью зависит от ее внешнего интерфейса: ее языковая семантика позволяет хорошо написанной программе Julia предоставлять компилятору больше возможностей для создания эффективного кода и макетов памяти. Если бы вы попытались скомпилировать код Matlab или Python для Julia, наш компилятор был бы ограничен семантикой Matlab или Python и выдавал бы код не лучше, чем у существующих компиляторов для этих языков (и, вероятно, хуже). Ключевая роль семантики также заключается в том, что некоторые существующие компиляторы Python (такие как Numba и Pythran) пытаются оптимизировать только небольшое подмножество языка (например, операции с массивами Numpy и скалярами), и для этого подмножества они уже делают свое дело по крайней мере так же хорошо, как мы могли бы для той же семантики. Люди, работающие над этими проектами, невероятно умны и достигли удивительных вещей, но переоснащение компилятора на язык, который был разработан для интерпретации, является очень сложной проблемой.
Преимущество Джулии заключается в том, что хорошая производительность не ограничивается небольшим подмножеством “встроенных” типов и операций, и есть возможность написать высокоуровневый универсальный с точки зрения типов код, который работает на произвольных пользовательских типах, оставаясь быстрым и эффективным с точки зрения памяти. Типы в таких языках, как Python, просто не предоставляют компилятору достаточно информации для подобных возможностей, поэтому, как только вы начинаете использовать эти языки в качестве интерфейса Julia, есть риск тяжело увязнуть.
По аналогичным причинам автоматический перевод на язык Julia также обычно генерирует нечитаемый, медленный, неидиоматический код, который не будет хорошей отправной точкой для родного порта Julia с другого языка.
С другой стороны, языковая интероперабельность чрезвычайно полезна: мы хотим использовать существующий высококачественный код на других языках от Julia (и наоборот)! Лучший способ получить это - не транспилер, а простые средства межъязыкового вызова. Мы упорно работали над этим, начиная со встроенного ccall
(для вызова библиотек C и Fortran) и заканчивая пакетами JuliaInterop, которые соединяют Julia с Python, Matlab, C++ и другими
У Julia нет аналога функции clear
из MATLABа; как только имя определено в сеансе Julia (технически, в модуле Main
), оно будет жить до конца сеанса.
Если вас беспокоит сожратая память, вы всегда можете заменить объекты теми, которые потребляют меньше памяти. Например, если A-это массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = nothing
. Память будет освобождена при следующем запуске сборщика мусора; вы можете принудительно выполнить это с помощью 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.jl может вдруг захотеться активировать дополнительную функциональность, такую как обработка аргументов командной строки. Способ определить, что файл выполняется таким образом, состоит в том, чтобы проверить, является ли abspath(PROGRAM_FILE) == @__FILE__
истинным.
Запуск скрипта через julia file.jl
не вызывает InterruptException
, когда вы пытаетесь завершить его с помощью CTRL-C (SIGINT). Чтобы запустить определенный код перед завершением сценария Julia, который может быть вызван или не вызван CTRL-C, используйте atexit. Кроме того, вы можете использовать julia-e ' include(popfirst!(ARGS))' file.jl
для выполнения скрипта, будучи в состоянии поймать InterruptException
в блоке try
.
Вариантов прохождения к julia в так называемый shebang, например, #!/usr/bin / env julia --startup-file=no
может не работать на некоторых платформах, таких как Linux. Это происходит потому, что синтаксический анализ аргументов в shebang зависит от платформы и не очень точно определен. В Unix-подобной среде надежным способом передачи параметров Джулии в исполняемом скрипте было бы запустить сценарий как сценарий bash и использовать exec для замены процесса на Джулию:
#!/bin/bash
#=
exec julia --color=yes --startup-file=no " ${BASH_SOURCE[0]}"" $@"
=#
@show ARGS # put any Julia code here
В приведенном выше примере код между #= и =# выполняется как сценарий bash. Джулия игнорирует эту часть, так как это многострочный комментарий для Джулии. Код Julia после =# игнорируется bash, так как он перестает анализировать файл, как только он достигает оператора exec.В приведенном выше примере код между #= и =# выполняется как сценарий Баш. Джулия игнорирует эту часть, так как это многострочный комментарий для Джулии. Код Джулия после =# игнорируется Баш, так как он перестает анализировать файл, как только он достигает оператора exec
.
Я передал аргумент 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
не может быть изменена путем ее передачи в качестве аргумента функции. При вызове change_value!(x)
в приведенном выше примере y
-это вновь созданная переменная, изначально привязанная к значению x
, то есть 10; затем y
переприравнивается к константе 17, в то время как переменная x
внешней области остается нетронутой.
Но вот на что вам следует обратить внимание: предположим, что x
привязан к объекту типа Array
(или любого другого изменяемого типа). Изнутри функции вы не можете "отвязать" x
от этого массива, но вы можете изменить его содержимое. Например:
julia> x = [1,2,3]
3-element Array{Int64,1}:
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 Array{Int64,1}:
5
2
3
Здесь мы создали функцию change_array!
, которая присваивает 5 первому элементу передаваемого массива (привязанному к x
в месте вызова и привязанному к A
внутри функции). Обратите внимание, что после вызова функции x
все еще привязан к тому же массиву, но содержимое этого массива изменилось: переменные A
и x
были различными привязками, относящимися к одному и тому же изменяемому объекту массива.
Нет, вам не разрешается иметь оператор 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
, но только внутри панели модулей.
Есть два вида использования этого оператора: чавканье и шлепанье. Многих новичков julia оператор ... запутывает. Это потому, что он означает две разные вещи в зависимости от контекста.
В контексте определений функций, ... оператор используется для объединения множества различных аргументов в один аргумент. Это применение ... для объединения множества различных аргументов в один аргумент называется прихлебыванием (slurping):
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
Если бы Джулия была языком, который более свободно использовал символы 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 Array{Int64,1}:
1
2
3
julia> threeargs(x...)
a = 1::Int64
b = 2::Int64
c = 3::Int64
Если бы Джулия была языком, который более свободно использовал символы ASCII, оператор splatting мог бы быть записан как ...->
вместо ...
.
Оператор = всегда возвращает правую часть, посему:
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 threetup()
x, y = [3, 3]
x, y # returns a tuple
end
threetup (generic function with 1 method)
julia> function threearr()
x, y = [3, 3] # returns an array
end
threearr (generic function with 1 method)
julia> threetup()
(3, 3)
julia> threearr()
2-element Array{Int64,1}:
3
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> 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 для выражения его числа измерений. При определении собственного параметрического типа можно использовать ограничения подтипа, чтобы объявить, что определенный параметр должен быть подтипом (<:)
некоторого абстрактного типа или параметром предыдущего типа. Однако не существует специального синтаксиса для объявления, что параметр должен быть значением заданного типа — то есть вы не можете напрямую объявить, что параметр, подобный размерности, является Int в определении структуры, например. Точно так же вы не можете выполнять вычисления (включая простые вещи, такие как сложение или вычитание) по параметрам типа. Вместо этого эти виды ограничений и отношений могут быть выражены с помощью дополнительных параметров типа, которые вычисляются и применяются в конструкторах типа.
В качестве примера рассмотрим
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))
Джулия использует машинную арифметику для целочисленных вычислений. Это означает, что диапазон значений Int ограничен и обтекается с обоих концов, так что сложение, вычитание и умножение целых чисел могут переполняться или уменьшаться, что приводит к некоторым результатам, которые поначалу могут быть тревожными:
julia> typemax(Int)
9223372036854775807
julia> ans+1
-9223372036854775808
julia> -ans
-9223372036854775808
julia> 2*ans
0
Очевидно, что это далеко не так, как ведут себя математические целые числа, и вы можете подумать, что это не совсем идеально для языка программирования высокого уровня, чтобы предоставить это пользователю. Однако для численной работы, где эффективность и прозрачность имеют первостепенное значение, альтернативы будут только хуже.
Одной из альтернатив, которую следует рассмотреть, было бы проверка каждой целочисленной операции на переполнение и продвинуть результаты к более крупным целочисленным типам, таким как Int128 или BigInt в случае переполнения. К сожалению, это приводит к значительным накладным расходам на каждую целочисленную операцию (например, увеличение счетчика циклов) – она требует испускания кода для выполнения проверок переполнения во время выполнения после арифметических инструкций и ветвей для обработки потенциальных переполнений. Хуже того, это приведет к тому, что каждое вычисление, включающее целые числа, будет нестабильно по типу. Как мы уже упоминали выше, стабильность типов имеет решающее значение для генерации эффективного кода. Если вы не можете рассчитывать на то, что результаты целочисленных операций будут целыми числами, то невозможно создать быстрый и простой код, как это делают компиляторы C и Fortran.
Одним из вариантов этого подхода, который позволяет избежать появления нестабильности типов, является слияние типов Int и BigInt в один гибридный целочисленный тип, который внутренне изменяет представление, когда результат больше не вписывается в размер машинного целого числа. Хотя это поверхностно позволяет избежать нестабильности типов на уровне кода Julia, это просто сметает проблему под ковер, навязывая все те же трудности коду C, реализующему этот гибридный целочисленный тип. Этот подход можно заставить работать и даже может быть сделан довольно быстро во многих случаях, но имеет несколько недостатков. Одна из проблем заключается в том, что представление целых чисел и массивов целых чисел в памяти больше не соответствует естественному представлению, используемому C, Fortran и другими языками с собственными машинными целыми числами. Таким образом, чтобы взаимодействовать с этими языками, нам в конечном счете все равно придется вводить собственные целочисленные типы. Любое неограниченное представление целых чисел не может иметь фиксированного числа битов и, следовательно, не может быть сохранено встроенным в массив с фиксированными слотами-большие целочисленные значения всегда будут требовать отдельного хранилища, выделенного для кучи. И конечно, независимо от того, насколько умной является гибридная целочисленная реализация, всегда существуют ловушки производительности – ситуации, когда производительность неожиданно снижается. Сложное представление, отсутствие взаимодействия с C и Fortran, неспособность представлять целочисленные массивы без дополнительного хранения кучи и непредсказуемые характеристики производительности делают даже самые умные гибридные целочисленные реализации плохим выбором для высокопроизводительной численной работы.
Альтернативой использованию гибридных целых чисел или продвижению к Бигинтам является использование насыщающей целочисленной арифметики, где добавление к наибольшему целочисленному значению оставляет его неизменным, а также для вычитания из наименьшего целочисленного значения. Именно это и делает 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, уже уничтожило информацию, необходимую для вычисления правильной средней точки.
Мало того, что отсутствие ассоциативности неудачно для программистов, которые не могут полагаться на нее для таких методов, как этот, но это также побеждает почти все, что компиляторы могли бы хотеть сделать для оптимизации целочисленной арифметики. Например, поскольку целые числа Джулии используют обычную машинную целочисленную арифметику, 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 на удаленном узле является то, что привязка с таким именем не существует. Давайте рассмотрим некоторые из возможных причин.
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 Foo
определил Foo на всех узлах. Однако вызов к Foo.foo()
создал новую глобальную привязку gvar
на локальном узле, но она не была найдена на узле 2, что привело к ошибке UndefVarError.
Обратите внимание,что это не относится к глобалам, созданным в модуле 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
Это не относится к объявлениям функций или структур. Однако анонимные функции, привязанные к глобальным переменным, сериализуются, как показано ниже.
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
Главный аргумент против + заключается в том, что конкатенация строк не является коммутативной, в то время как + обычно используется в качестве коммутативного оператора. Хотя сообщество Julia признает, что другие языки используют разные операторы и * могут быть незнакомы некоторым пользователям, оно передает определенные алгебраические свойства.
Обратите внимание, что вы также можете использовать string(...)
для объединения строк (и других значений, преобразованных в строки); аналогично, repeat
можно использовать вместо ^
для повторения строк. Синтаксис интерполяции также полезен для построения строк.
Есть только одно различие, и на первый взгляд (с точки зрения синтаксиса) оно может показаться очень незначительным. Разница между using и import заключается в том, что при использовании вам нужно сказать function Foo.бар(..
расширить функциональную панель модуля Foo с помощью нового метода, но с помощью импорта Foo.bar
, вам нужно только сказать function bar(...
и он автоматически расширяет функциональную панель модуля Foo.
Причина, по которой достаточно важно иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширять функцию, о существовании которой вы не знали, потому что это может легко вызвать ошибку. Это, скорее всего, произойдет с методом, который принимает общий тип, такой как строка или целое число, потому что и вы, и другой модуль могли бы определить метод для обработки такого общего типа. Если вы используете import
, то вы замените реализацию bar другого модуля (s:: AbstractString)
своей новой реализацией, которая легко может сделать что-то совершенно другое (и сломать все/многие будущие использования других функций в модуле Foo, которые зависят от вызова bar).
В отличие от многих языков (например, C и Java), объекты Julia не могут быть "нулевыми" по умолчанию. Когда ссылка (переменная, поле объекта или элемент массива) неинициализирована, обращение к ней немедленно вызовет ошибку. Эта ситуация может быть обнаружена с помощью функций isdefined
или isassigned
.
Некоторые функции используются только для их побочных эффектов, и не нужно возвращать значение. В этих случаях соглашение состоит в том, чтобы вернуть значение nothing
, которое является просто одноэлементным объектом типа Nothing
. Это обычный тип без полей; в нем нет ничего особенного, кроме этого соглашения, и что REPL ничего не печатает для него. Некоторые языковые конструкции, которые в противном случае не имели бы значения, также ничего не дают, например, if false; end
.
В ситуациях, когда значение x
типа T
существует только иногда, тип Union{T, Nothing}
может использоваться для аргументов функций, полей объектов и типов элементов массива как эквивалент Nullable
, Option
или, возможно, в других языках. Если само значение может быть ничем (в частности, когда T является любым), то тип Union{Some{T}, Nothing}
является более подходящим, поскольку x == nothing
тогда указывает на отсутствие значения, а x == Some(nothing)
указывает на наличие значения, равного nothing
. Функция something
позволяет развернуть некоторые объекты и использовать значение по умолчанию вместо аргументов nothing
. Обратите внимание, что компилятор способен генерировать эффективный код при работе с аргументами или полями Union{T, Nothing}
.
Чтобы представить отсутствующие данные в статистическом смысле (NA в R или NULL в SQL), используйте отсутствующий объект. Дополнительные сведения см. В разделе пропущенные значения.
Пустой кортеж (())
- это еще одна форма небытия. Но на самом деле его не следует рассматривать как ничто, а скорее как кортеж нулевых значений.
Пустой (или" нижний") тип, записываемый как Union {}
(пустой тип объединения), является типом без значений и без подтипов (кроме самого себя). Как правило, вам не нужно будет использовать этот тип.
В Julia x += y заменяется во время синтаксического анализа на x = x + y. для массивов это приводит к тому, что вместо хранения результата в том же месте памяти, что и x, он выделяет новый массив для хранения результата.
Хотя такое поведение может кого-то удивить, выбор сделан сознательно. Основная причина заключается в наличии неизменяемых объектов внутри Джулии, которые не могут изменить свое значение после создания. Действительно, число является неизменяемым объектом; утверждения 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
"блокируют" поток во время вызова. Следовательно изменение записи в 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)
. В частности, нуль-мерные массивы не являются пустыми. Если вы находите это неинтуитивным, вот некоторые идеи, которые могут помочь понять определение Джулии.
- Нуль-мерные массивы являются "точкой" для "линии" вектора и "плоскости" матрицы. Точно так же, как линия не имеет площади (но все же представляет собой набор вещей), точка не имеет длины или каких-либо измерений вообще (но все же представляет собой вещь).
- Мы определяем
prod (())
равным 1, а общее число элементов в массиве является произведением его размера. Размер нулевого массива равен()
, и поэтому его длина равна 1. - Нуль-мерные массивы изначально не имеют никаких измерений, в которые вы индексируете - они просто []. Мы можем применить к ним то же правило "trailing one", что и ко всем другим размерностям массива, так что вы действительно можете индексировать их как 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)
является ошибкой.
Вы можете обнаружить, что простые бенчмарки линейной алгебры выглядят как-то так
using BenchmarkTools
A = randn(1000, 1000)
B = randn(1000, 1000)
@btime $A \ $B
@btime $A * $B
и они могут отличаться от таковых в Matlab или R.
Поскольку подобные операции являются очень тонкими обертками над соответствующими функциями BLAS, причиной расхождения, скорее всего, будет
- библиотека BLAS, которую использует каждый язык,
- количество параллельных потоков.
Джулия компилирует и использует свою собственную копию OpenBLAS, с потоками, которые в настоящее время ограничены 8 (или количеством ваших ядер).
Изменение настроек OpenBLAS или компиляция Julia с другой библиотекой BLAS, например Intel MKL, может обеспечить повышение производительности. Вы можете использовать MKL.jl, пакет, который заставляет линейную алгебру Джулии использовать Intel MKL BLAS и LAPACK вместо OpenBLAS, или поискать на дискуссионном форуме предложения о том, как настроить это вручную. Обратите внимание, что Intel MKL не может быть поставлен в комплекте с 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. 0.x; эта ветвь будет продолжать получать исправления ошибок до тех пор, пока не будет выбрана новая ветвь LTS, после чего v1.0.серия x больше не будет получать регулярные исправления ошибок, и всем, кроме самых консервативных пользователей, будет рекомендовано перейти на новую серию версий LTS. Как разработчик пакетов, вы можете предпочесть разработку для версии LTS, чтобы максимально увеличить число пользователей, которые могут использовать ваш пакет. Согласно SemVer, код, написанный для версии v1. 0, будет продолжать работать для всех будущих LTS и стабильных версий. В общем, даже если вы нацелены на LTS, вы можете разрабатывать и запускать код в последней стабильной версии, чтобы воспользоваться преимуществами улучшенной производительности; до тех пор, пока вы избегаете использования новых функций (таких как добавленные библиотечные функции или новые методы).
Вы можете предпочесть ночную версию Julia, если хотите воспользоваться последними обновлениями языка, и не возражаете, если версия, доступная сегодня, иногда на самом деле не работает. Как следует из названия, релизы для ночной версии производятся примерно каждую ночь (в зависимости от стабильности инфраструктуры сборки). В целом ночные релизы довольно безопасны в использовании—ваш код не загорится. Однако они могут быть случайными регрессиями и / или проблемами, которые не будут обнаружены до более тщательного предварительного тестирования. Возможно, вы захотите протестировать ночную версию, чтобы убедиться, что такие регрессии, влияющие на ваш вариант использования, будут пойманы до выпуска релиза.
Наконец, вы также можете рассмотреть возможность создания Джулии из исходного кода для себя. Этот вариант в основном предназначен для тех людей, которые чувствуют себя комфортно в командной строке или заинтересованы в обучении. Если это описывает вас, вам также может быть интересно прочитать наши рекомендации по внесению вклада.
Ссылки на каждый из этих типов загрузки можно найти на странице загрузки по адресу https://julialang.org/downloads обратите внимание, что не все версии Julia доступны для всех платформ.