Часто задаваемые вопросы
Общие
Является ли 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.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 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 Vector{Int64}:
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> 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
Почему Julia использует * для конкатенации строк? Почему не + или что-то другое?
Основной аргумент против + заключается в том, что конкатенация строк не коммутативна, в то время как + обычно используется как коммутативный оператор. Хотя сообщество Julia признаёт, что другие языки используют разные операторы, и * может быть непривычным для некоторых пользователей, это выражает определённые алгебраические свойства.
Обратите внимание, что вы также можете использовать string(...) для конкатенации строк (и других значений, преобразованных в строки); аналогично, repeat может быть использовано вместо ^ для повторения строк. Синтаксис интерполяции также полезен для построения строк.
Пакеты и модули
В чём разница между "using" и "import"?
Разница всего одна, и на первый взгляд (с точки зрения синтаксиса) она может показаться очень незначительной. Разница между using и import заключается в том, что с помощью using вам нужно указать function Foo.bar(.. для расширения функции модуля Foo bar новым методом, но с помощью import Foo.bar, вам нужно указать только function bar(... и он автоматически расширит функцию модуля Foo bar.
Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширить функцию, о существовании которой вы не знали, потому что это может легко привести к ошибке. Это наиболее вероятно произойдёт с методом, принимающим общий тип, такой как строка или целое число, так как и вы, и другой модуль можете определить метод для обработки такого общего типа. Если вы используете 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]вычисляется и сохраняется в левой части до завершения использования в правой части. - Предположим, вы готовы выделить временное хранилище для вычислений (что устранит большую часть смысла в использовании
*=в режиме in-place); если вы воспользуетесь изменяемостью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,1], и т. д.; см. Пропущенные и дополнительные индексы.
Также важно понять различия со стандартными скалярами. Скаляры не являются изменяемыми контейнерами (хотя они итерируемы и определяют такие вещи, как length, getindex, например, 1[] == 1). В частности, если x = 0.0 определен как скаляр, попытка изменить его значение через x[] = 1.0 является ошибкой. Скаляр x может быть преобразован в нульмерный массив, содержащий его, с помощью fill(x), и наоборот, нульмерный массив a может быть преобразован в содержащийся в нем скаляр с помощью a[].
Почему мои бенчмарки 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
Хотите ли вы использовать стабильную, 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 доступны для всех платформ.
Как перенести список установленных пакетов после обновления моей версии 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.6.0/manual/faq/