Часто задаваемые вопросы
Общие
Язык 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++ и многим другим.
Сессии и REPL
Как удалить объект из памяти?
В Julia нет аналога функции MATLAB clear; как только имя определено в сессии 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 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 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 были различными ссылками, ссылающимися на один и тот же изменяемый 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.
Что делает оператор ...?
Два способа использования оператора ...: slurping и splatting
Многим новичкам в 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
Если бы Julia использовала ASCII-символы более свободно, оператор slurping можно было бы записать как <-... вместо ....
... разделяет один аргумент на множество различных аргументов в вызовах функций
В отличие от использования оператора ... для обозначения объединения многих аргументов в один аргумент при определении функции, оператор ... также используется для разделения одного аргумента функции на множество различных аргументов в контексте вызова функции. Это использование оператора ... называется splatting:
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
Если бы Julia использовала 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 не может предсказать тип результата в момент компиляции, любое вычисление, которое его использует, должно учитывать возможность обоих типов, что затрудняет генерацию быстрого машинного кода.
Почему 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> 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, невозможность представления массивов целых чисел без дополнительного выделения в куче и непредсказуемая производительность делают даже самые изобретательные реализации гибридных типов целых чисел плохим выбором для высокопроизводительных численных вычислений.
Альтернативой использованию гибридных целых чисел или продвижению к 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
Почему 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 для получения дополнительной информации.
Пустой кортеж (()) является ещё одной формой ничто. Однако его не следует рассматривать как ничто, а скорее как кортеж из нулевых значений.
Пустой (или "нижний") тип, записываемый как 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
Желательно ли использовать стабильную, 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.0.x; этот ветвь будет продолжать получать исправления ошибок до тех пор, пока не будет выбран новый LTS-ветвь, в этот момент серия v1.0.x больше не будет получать регулярных исправлений ошибок, и всем, кроме самых консервативных пользователей, будет рекомендовано обновиться до новой серии LTS-версий. Как разработчику пакетов, вы можете предпочесть разрабатывать для LTS-версии, чтобы максимально увеличить количество пользователей, которые могут использовать ваш пакет. В соответствии с SemVer, код, написанный для v1.0, будет продолжать работать для всех будущих LTS и стабильных версий. В общем случае, даже если вы нацеливаетесь на LTS, вы можете разрабатывать и запускать код в последней стабильной версии, чтобы воспользоваться улучшенной производительностью; при условии, что вы избегаете использования новых функций (таких как добавленные функции библиотек или новые методы).
Вы можете предпочесть ночную версию Julia, если хотите воспользоваться последними обновлениями языка и не против, если доступная сегодня версия иногда не работает. Как следует из названия, выпуски в ночной версии производятся примерно каждую ночь (в зависимости от стабильности инфраструктуры сборки). В целом, ночные выпуски довольно безопасны в использовании — ваш код не загорится. Однако могут быть случайные регрессии или проблемы, которые не будут обнаружены до более тщательного тестирования в предварительном релизе. Возможно, вы захотите протестировать ночную версию, чтобы убедиться, что такие регрессии, влияющие на ваш случай использования, будут обнаружены до выпуска версии.
Наконец, вы также можете рассмотреть возможность сборки Julia из исходных кодов самостоятельно. Этот вариант подходит в основном тем людям, которые чувствуют себя комфортно в командной строке или заинтересованы в обучении. Если это о вас, вам также может быть интересно ознакомиться с нашими руководящими принципами по участию.
Ссылки на каждый из этих типов загрузок можно найти на странице загрузки по адресу https://julialang.org/downloads/. Обратите внимание, что не все версии Julia доступны для всех платформ.
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.4.2/manual/faq/