Часто задаваемые вопросы
Общие
Является ли 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.
Что делает оператор ...?
Два использования оператора ...: поглощение и разброс
Многим новичкам в 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 Array{Int64,1}:
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 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.
В некоторых языках пустая кортеж (()) считается канонической формой ничто. Однако в Julia лучше рассматривать её как обычную кортеж, содержащую ноль значений.
Пустой (или «нижний») тип, записанный как Union{} (пустой тип объединения), является типом без значений и без подтипов (кроме самого себя). Вам, как правило, не потребуется использовать этот тип.
Память
Почему x += y выделяет память, когда x и y являются массивами?
В Julia x += y заменяется во время парсинга на x = x + y. Для массивов это означает, что вместо хранения результата в том же месте в памяти, что и x, выделяется новый массив для хранения результата.
Хотя это поведение может удивить некоторых, выбор сделан намеренно. Основная причина — наличие неизменяемых объектов в Julia, которые не могут изменить своё значение после создания. Действительно, число — неизменяемый объект; операторы x = 5; x += 1 не изменяют значение 5, они изменяют значение, связанное с x. Для неизменяемого объекта единственный способ изменить значение — переназначить его.
Для дальнейшего углубления рассмотрим следующую функцию:
function power_by_squaring(x, n::Int)
ispow2(n) || error("This implementation only works for powers of 2")
while n >= 2
x *= x
n >>= 1
end
x
end
После вызова типа x = 5; y = power_by_squaring(x, 4), вы получите ожидаемый результат: x == 5 && y == 625. Однако, предположим, что *=, когда используется с матрицами, вместо этого изменяет левую часть. Возникнут две проблемы:
- Для общих квадратных матриц
A = A*Bне может быть реализовано без временного хранения:A[1,1]вычисляется и хранится в левой части, прежде чем вы закончите его использование в правой части. - Предположим, что вы готовы выделить временное хранилище для вычислений (что устранило бы большую часть преимущества использования
*=непосредственно); если вы воспользуетесь изменяемостьюx, то эта функция будет вести себя по-разному для изменяемых и неизменяемых входных данных. В частности, для неизменяемыхx, после вызова у вас будет (вообще)y != x, но для изменяемыхxу вас будетy == x.
Поскольку поддержка обобщенного программирования считается более важной, чем потенциальные оптимизации производительности, которые могут быть достигнуты другими средствами (например, с использованием явных циклов), операторы, такие как += и *= работают, переопределяя новые значения.
Асинхронное ввод-вывод и одновременные синхронные записи
Почему одновременные записи в один и тот же поток приводят к перепутанному выводу?
Хотя API потокового ввода-вывода является синхронным, его внутренняя реализация полностью асинхронна.
Рассмотрим печатный вывод из следующего:
julia> @sync for i in 1:3
@async write(stdout, string(i), " Foo ", " Bar ")
end
123 Foo Foo Foo Bar Bar Bar
Это происходит потому, что, хотя вызов write является синхронным, запись каждого аргумента уступает другим задачам во время ожидания завершения этой части ввода-вывода.
print и println «блокируют» поток во время вызова. Вследствие этого изменение write на println в приведенном выше примере приводит к:
julia> @sync for i in 1:3
@async println(stdout, string(i), " Foo ", " Bar ")
end
1 Foo Bar
2 Foo Bar
3 Foo Bar
Вы можете заблокировать свои записи с помощью ReentrantLock следующим образом:
julia> l = ReentrantLock();
julia> @sync for i in 1:3
@async begin
lock(l)
try
write(stdout, string(i), " Foo ", " Bar ")
finally
unlock(l)
end
end
end
1 Foo Bar 2 Foo Bar 3 Foo Bar
Массивы
В чем разница между нульмерными массивами и скалярами?
Нульмерные массивы — это массивы вида Array{T,0}. Они ведут себя как скаляры, но существуют важные различия. Они заслуживают особого упоминания, так как представляют собой особый случай, который имеет логический смысл в свете общего определения массивов, но может быть несколько неинтуитивным на первый взгляд. Следующая строка определяет нульмерный массив:
julia> A = zeros()
0-dimensional Array{Float64,0}:
0.0
В этом примере A — это изменяемый контейнер, содержащий один элемент, который можно установить с помощью A[] = 1.0 и извлечь с помощью A[]. Все нульмерные массивы имеют одинаковый размер (size(A) == ()) и длину (length(A) == 1). В частности, нульмерные массивы не пустые. Если вам это кажется неинтуитивным, вот несколько идей, которые могут помочь понять определение массивов в Julia.
- Нульмерные массивы — это «точка» к «линии» вектора и «плоскости» матрицы. Так же, как у линии нет площади (но она всё же представляет множество вещей), у точки нет длины или каких-либо измерений (но она всё же представляет собой одну вещь).
- Мы определяем
prod(())как 1, а общее количество элементов в массиве — это произведение размеров. Размер нульмерного массива —(), и, следовательно, его длина —1. - Нульмерные массивы по своей природе не имеют измерений, в которых можно индексировать — они просто
A[]. Мы можем применить то же правило «последующей единицы» к ним, что и ко всем другим размерностям массивов, поэтому вы действительно можете индексировать их какA[1,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 в высокопроизводительных вычислительных (ВВС) установках одновременное вызов 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 доступны для всех платформ.
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/faq/