Frequently Asked Questions
General
Is Julia named after someone or something?
Нет.
Why don't you compile Matlab/Python/R/… code to Julia?
Поскольку многие люди знакомы с синтаксисом других динамических языков, и уже написано много кода на этих языках, естественно задаться вопросом, почему мы просто не подключили интерфейс Matlab или Python к бэкенду Julia (или не "транспилировали" код в Julia), чтобы получить все преимущества производительности Julia, не требуя от программистов изучения нового языка. Просто, верно?
Основная проблема в том, что в компиляторе Julia нет ничего особенного: мы используем обычный компилятор (LLVM) без “секретного соуса”, о котором не знают другие разработчики языков. Действительно, компилятор Julia во многом гораздо проще, чем у других динамических языков (например, PyPy или LuaJIT). Преимущество производительности Julia почти полностью вытекает из его фронтенда: семантика языка позволяет well-written Julia program предоставлять больше возможностей компилятору для генерации эффективного кода и распределения памяти. Если бы вы попытались скомпилировать код Matlab или Python в Julia, наш компилятор был бы ограничен семантикой Matlab или Python, производя код не лучше, чем существующие компиляторы для этих языков (и, вероятно, хуже). Ключевая роль семантики также объясняет, почему несколько существующих компиляторов Python (таких как Numba и Pythran) пытаются оптимизировать лишь небольшую подсоставляющую языка (например, операции с массивами Numpy и скалярами), и для этой подсоставляющей они уже показывают как минимум такие же результаты, как мы могли бы для тех же семантик. Люди, работающие над этими проектами, невероятно умны и достигли удивительных вещей, но адаптация компилятора к языку, который был разработан для интерпретации, является очень сложной задачей.
Преимущество Julia заключается в том, что хорошая производительность не ограничивается небольшой подсистемой «встроенных» типов и операций, и можно писать высокоуровневый обобщенный код, который работает с произвольными пользовательскими типами, оставаясь при этом быстрым и экономичным по памяти. Типы в языках, таких как Python, просто не предоставляют компилятору достаточно информации для аналогичных возможностей, поэтому, как только вы используете эти языки в качестве интерфейса для Julia, вы окажетесь в затруднительном положении.
По аналогичным причинам автоматический перевод на Julia также обычно генерирует нечитаемый, медленный, неидиоматичный код, который не будет хорошей отправной точкой для нативного порта Julia с другого языка.
С другой стороны, взаимодействие языков чрезвычайно полезно: мы хотим использовать существующий высококачественный код на других языках из Julia (и наоборот)! Лучший способ сделать это — не транспилятор, а скорее простые средства межъязыкового вызова. Мы усердно работали над этим, начиная с встроенного ccall
(для вызова библиотек C и Fortran) до JuliaInterop пакетов, которые соединяют Julia с Python, Matlab, C++ и многими другими.
Public API
How does Julia define its public API?
Общедоступный API Джулии — это поведение, описанное в документации общедоступных символов из Base
и стандартных библиотек. Функции, типы и константы не являются частью общедоступного API, если они не являются общедоступными, даже если у них есть строки документации или они описаны в документации. Более того, только задокументированное поведение общедоступных символов является частью общедоступного API. Незадокументированное поведение общедоступных символов является внутренним.
Публичные символы — это те, которые помечены либо public foo
, либо export foo
.
Другими словами:
- Документированное поведение публичных символов является частью публичного API.
- Не задокументированное поведение публичных символов не является частью публичного API.
- Документированное поведение частных символов не является частью публичного API.
- Не задокументированное поведение частных символов не является частью публичного API.
Вы можете получить полный список публичных символов из модуля с помощью names(MyModule)
.
Авторы пакетов поощряются определять свой публичный API аналогичным образом.
Все в публичном API Julia охватывается SemVer и, следовательно, не будет удалено или подвергнуто значительным разрушающим изменениям до выхода Julia 2.0.
There is a useful undocumented function/type/constant. Can I use it?
Обновление Julia может сломать ваш код, если вы используете непубличный API. Если код является самодостаточным, может быть хорошей идеей скопировать его в ваш проект. Если вы хотите полагаться на сложный непубличный API, особенно при использовании его из стабильного пакета, хорошей идеей будет открыть issue или pull request, чтобы начать обсуждение о превращении его в публичный API. Однако мы не отговариваем от попыток создать пакеты, которые открывают стабильные публичные интерфейсы, полагаясь на непубличные детали реализации Julia и сглаживая различия между разными версиями Julia.
The documentation is not accurate enough. Can I rely on the existing behavior?
Пожалуйста, откройте issue или pull request, чтобы начать обсуждение о превращении существующего поведения в публичный API.
Sessions and the REPL
How do I delete an object in memory?
Julia не имеет аналога функции clear
из MATLAB; как только имя определено в сессии Julia (технически, в модуле Main
), оно всегда присутствует.
Если использование памяти является вашей проблемой, вы всегда можете заменить объекты на те, которые потребляют меньше памяти. Например, если A
— это массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = nothing
. Память будет освобождена в следующий раз, когда запустится сборщик мусора; вы можете заставить это произойти с помощью GC.gc()
. Более того, попытка использовать A
вероятно приведет к ошибке, потому что большинство методов не определены для типа Nothing
.
How can I modify the declaration of a type in my session?
Возможно, вы определили тип, а затем поняли, что нужно добавить новое поле. Если вы попробуете это в 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)
...
Scripting
How do I check if the current file is being run as the main script?
Когда файл выполняется как основной скрипт с помощью julia file.jl
, может возникнуть необходимость активировать дополнительную функциональность, такую как обработка аргументов командной строки. Способ определить, что файл выполняется таким образом, — это проверить, равно ли abspath(PROGRAM_FILE) == @__FILE__
значению true
.
Однако рекомендуется не писать файлы, которые одновременно являются скриптом и импортируемой библиотекой. Если требуется функциональность, доступная как в библиотеке, так и в скрипте, лучше написать это как библиотеку, а затем импортировать функциональность в отдельный скрипт.
How do I catch CTRL-C in a script?
Запуск скрипта Julia с помощью julia file.jl
не вызывает InterruptException
, когда вы пытаетесь завершить его с помощью CTRL-C (SIGINT). Чтобы выполнить определенный код перед завершением скрипта Julia, который может быть вызван или не вызван CTRL-C, используйте atexit
. В качестве альтернативы вы можете использовать julia -e 'include(popfirst!(ARGS))' file.jl
, чтобы выполнить скрипт, имея возможность поймать InterruptException
в блоке try
. Обратите внимание, что с этой стратегией PROGRAM_FILE
не будет установлен.
How do I pass options to julia
using #!/usr/bin/env
?
Передача параметров в julia
в так называемой строке shebang, как в #!/usr/bin/env julia --startup-file=no
, не будет работать на многих платформах (BSD, macOS, Linux), где ядро, в отличие от оболочки, не разделяет аргументы по пробелам. Опция env -S
, которая разделяет одну строку аргумента на несколько аргументов по пробелам, аналогично оболочке, предлагает простое решение:
#!/usr/bin/env -S julia --color=yes --startup-file=no
@show ARGS # put any Julia code here
Опция env -S
появилась в FreeBSD 6.0 (2005), macOS Sierra (2016) и GNU/Linux coreutils 8.30 (2018).
Why doesn't run
support *
or pipes for scripting external programs?
Функция Julia run
запускает внешние программы непосредственно, без вызова operating-system shell (в отличие от функции system("...")
в других языках, таких как Python, R или C). Это означает, что run
не выполняет расширение подстановочных знаков *
("globbing"), и не интерпретирует shell pipelines как |
или >
.
Вы все еще можете использовать глобирование и конвейеры с помощью функций Julia. Например, встроенная функция pipeline
позволяет вам связывать внешние программы и файлы, аналогично конвейерам оболочки, а Glob.jl package реализует совместимое с POSIX глобирование.
Вы можете, конечно, запускать программы через оболочку, явно передавая оболочку и строку команды в run
, например, run(`sh -c "ls > files.txt"`)
для использования Unix Bourne shell, но в общем случае вы должны предпочитать чистый скрипт на Julia, такой как run(pipeline(`ls`, "files.txt"))
. Причина, по которой мы избегаем оболочки по умолчанию, заключается в том, что shelling out sucks: запуск процессов через оболочку медленный, хрупкий к экранированию специальных символов, имеет плохую обработку ошибок и проблематичен для портативности. (Разработчики Python пришли к similar conclusion.)
Variables and Assignments
Why am I getting UndefVarError
from a simple loop?
Вы можете иметь что-то вроде:
x = 0
while x < 10
x += 1
end
и обратите внимание, что это работает нормально в интерактивной среде (например, в Julia REPL), но выдает UndefVarError: `x` not defined
, когда вы пытаетесь запустить это в скрипте или другом файле. Что происходит, так это то, что Julia обычно требует, чтобы вы явно присваивали значения глобальным переменным в локальной области видимости.
Здесь x
является глобальной переменной, while
определяет local scope, а x += 1
является присваиванием глобальной переменной в этой локальной области видимости.
Как упоминалось выше, Julia (версии 1.5 или новее) позволяет опустить ключевое слово global
для кода в REPL (и многих других интерактивных средах), чтобы упростить исследование (например, копирование и вставка кода из функции для интерактивного выполнения). Однако, как только вы переходите к коду в файлах, Julia требует более дисциплинированного подхода к глобальным переменным. У вас есть как минимум три варианта:
- Поместите код в функцию (так что
x
будет локальной переменной в функции). В общем, хорошей практикой программирования является использование функций вместо глобальных скриптов (поиск в интернете по запросу "почему глобальные переменные плохи" даст множество объяснений). В Julia глобальные переменные также являются slow. - Оберните код в блоке
let
. (Это делаетx
локальной переменной внутри оператораlet ... end
, снова устраняя необходимость вglobal
). - Явно отметьте
x
какglobal
внутри локальной области видимости перед присвоением, например, напишитеglobal x += 1
.
Более подробное объяснение можно найти в разделе руководства on soft scope.
Functions
I passed an argument x
to a function, modified it inside that function, but on the outside, the variable x
is still unchanged. Why?
Предположим, вы вызываете функцию так:
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
.
Can I use using
or import
inside a function?
Нет, вам не разрешается использовать оператор using
или import
внутри функции. Если вы хотите импортировать модуль, но использовать его символы только внутри конкретной функции или набора функций, у вас есть два варианта:
Используйте
import
:julia import Foo function bar(...) # ... refer to Foo symbols via Foo.baz ... end
Это загружает модуль
Foo
и определяет переменнуюFoo
, которая ссылается на модуль, но не импортирует ни один из других символов из модуля в текущее пространство имен. Вы обращаетесь к символамFoo
по их квалифицированным именамFoo.bar
и т.д.Оберните вашу функцию в модуль:
julia module Bar export bar using Foo function bar(...) # ... refer to Foo.baz as simply baz .... end end using Bar
Это импортирует все символы из
Foo
, но только внутри модуляBar
.
What does the ...
operator do?
The two uses of the ...
operator: slurping and splatting
Многие новички в Julia находят использование оператора ...
запутанным. Часть того, что делает оператор ...
запутанным, заключается в том, что он имеет два разных значения в зависимости от контекста.
...
combines many arguments into one argument in function definitions
В контексте определения функций оператор ...
используется для объединения множества различных аргументов в один аргумент. Это использование ...
для объединения множества различных аргументов в один аргумент называется слурпингом:
julia> function printargs(args...)
println(typeof(args))
for (i, arg) in enumerate(args)
println("Arg #$i = $arg")
end
end
printargs (generic function with 1 method)
julia> printargs(1, 2, 3)
Tuple{Int64, Int64, Int64}
Arg #1 = 1
Arg #2 = 2
Arg #3 = 3
Если бы Джулия была языком, который более свободно использует символы ASCII, оператор "сосания" мог бы быть записан как <-...
вместо ...
.
...
splits one argument into many different arguments in function calls
В отличие от использования оператора ...
для обозначения объединения многих различных аргументов в один аргумент при определении функции, оператор ...
также используется для того, чтобы один аргумент функции был разделен на множество различных аргументов при использовании в контексте вызова функции. Это использование ...
называется распаковкой:
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, оператор распаковки мог бы быть записан как ...->
вместо ...
.
What is the return value of an assignment?
Оператор =
всегда возвращает правую часть, следовательно:
julia> function threeint()
x::Int = 3.0
x # returns variable x
end
threeint (generic function with 1 method)
julia> function threefloat()
x::Int = 3.0 # returns 3.0
end
threefloat (generic function with 1 method)
julia> threeint()
3
julia> threefloat()
3.0
и аналогично:
julia> function twothreetup()
x, y = [2, 3] # assigns 2 to x and 3 to y
x, y # returns a tuple
end
twothreetup (generic function with 1 method)
julia> function twothreearr()
x, y = [2, 3] # returns an array
end
twothreearr (generic function with 1 method)
julia> twothreetup()
(2, 3)
julia> twothreearr()
2-element Vector{Int64}:
2
3
Types, type declarations, and constructors
What does "type-stable" mean?
Это означает, что тип вывода предсказуем из типов входных данных. В частности, это означает, что тип вывода не может изменяться в зависимости от значений входных данных. Следующий код не является типобезопасным:
julia> function unstable(flag::Bool)
if flag
return 1
else
return 1.0
end
end
unstable (generic function with 1 method)
Он возвращает либо Int
, либо Float64
в зависимости от значения его аргумента. Поскольку Julia не может предсказать тип возвращаемого значения этой функции во время компиляции, любые вычисления, которые ее используют, должны быть способны справляться со значениями обоих типов, что затрудняет создание быстрого машинного кода.
Why does Julia give a DomainError
for certain seemingly-sensible operations?
Некоторые операции имеют математический смысл, но приводят к ошибкам:
julia> sqrt(-2.0)
ERROR: DomainError with -2.0:
sqrt was called with a negative real argument but 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
. Можно было бы написать функцию 4d61726b646f776e2e436f64652822222c2022737172742229_40726566
, которая переключалась бы на комплексный вывод только при передаче отрицательного числа (что делает 4d61726b646f776e2e436f64652822222c2022737172742229_40726566
в некоторых других языках), но тогда результатом не было бы type-stable, и функция 4d61726b646f776e2e436f64652822222c2022737172742229_40726566
имела бы плохую производительность.
В этих и других случаях вы можете получить желаемый результат, выбрав тип ввода, который передает вашу готовность принять тип вывода, в котором результат может быть представлен:
julia> sqrt(-2.0+0im)
0.0 + 1.4142135623730951im
How can I constrain or compute type parameters?
Параметры parametric type могут содержать либо типы, либо значения битов, и сам тип выбирает, как использовать эти параметры. Например, Array{Float64, 2}
параметризован типом Float64
, чтобы выразить его тип элементов, и целочисленным значением 2
, чтобы выразить его количество измерений. При определении собственного параметрического типа вы можете использовать ограничения подтипов, чтобы объявить, что определенный параметр должен быть подтипом (<:
) некоторого абстрактного типа или предыдущего параметра типа. Однако нет специального синтаксиса для объявления, что параметр должен быть значением данного типа — то есть вы не можете напрямую объявить, что параметр, подобный размерности isa
, должен быть Int
в определении struct
, например. Аналогично, вы не можете выполнять вычисления (включая простые вещи, такие как сложение или вычитание) с параметрами типа. Вместо этого такие ограничения и отношения могут быть выражены через дополнительные параметры типа, которые вычисляются и обеспечиваются в constructors типа.
В качестве примера, рассмотрим
struct ConstrainedType{T,N,N+1} # NOTE: INVALID SYNTAX
A::Array{T,N}
B::Array{T,N+1}
end
где пользователь хотел бы обеспечить, чтобы третий параметр типа всегда был равен второму плюс один. Это можно реализовать с помощью явного параметра типа, который проверяется с помощью inner constructor method (где его можно комбинировать с другими проверками):
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
Этот контроль обычно бесплатен, так как компилятор может исключить проверку на допустимые конкретные типы. Если второй аргумент также вычисляется, может быть целесообразно предоставить outer constructor method, который выполняет это вычисление:
ConstrainedType(A) = ConstrainedType(A, compute_B(A))
Why does Julia use native machine integer arithmetic?
Джулия использует машинную арифметику для целочисленных вычислений. Это означает, что диапазон значений Int
ограничен и оборачивается на обоих концах, так что сложение, вычитание и умножение целых чисел могут привести к переполнению или недополнению, что может привести к некоторым результатам, которые вначале могут быть неприятными:
julia> x = typemax(Int)
9223372036854775807
julia> y = x+1
-9223372036854775808
julia> z = -y
-9223372036854775808
julia> 2*z
0
Очевидно, это далеко от того, как ведут себя математические целые числа, и вы можете подумать, что это менее чем идеально для языка программирования высокого уровня, чтобы открывать это пользователю. Однако для численных работ, где эффективность и прозрачность имеют первостепенное значение, альтернативы хуже.
Одной из альтернатив, которую стоит рассмотреть, было бы проверять каждую целочисленную операцию на переполнение и повышать результаты до более крупных целочисленных типов, таких как Int128
или BigInt
в случае переполнения. К сожалению, это вводит значительные накладные расходы на каждую целочисленную операцию (подумайте о увеличении счетчика цикла) – это требует генерации кода для выполнения проверок переполнения во время выполнения после арифметических инструкций и ветвлений для обработки потенциальных переполнений. Что еще хуже, это приведет к тому, что каждое вычисление, связанное с целыми числами, станет нестабильным по типу. Как мы упоминали выше, type-stability is crucial для эффективной генерации эффективного кода. Если вы не можете полагаться на то, что результаты целочисленных операций будут целыми числами, невозможно сгенерировать быстрый, простой код так, как это делают компиляторы 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)
Поскольку компилятор знает, что сложение и умножение целых чисел являются ассоциативными, а умножение распределяется по сложению – ни одно из которых не верно для насыщенной арифметики – он может оптимизировать весь цикл до всего лишь одного умножения и одного сложения. Насыщенная арифметика полностью разрушает этот вид оптимизации, поскольку ассоциативность и распределительность могут нарушаться на каждой итерации цикла, вызывая разные результаты в зависимости от того, на какой итерации происходит сбой. Компилятор может развернуть цикл, но он не может алгебраически сократить множество операций до меньшего количества эквивалентных операций.
Наиболее разумной альтернативой тому, чтобы целочисленная арифметика тихо переполнялась, является использование проверенной арифметики повсюду, вызывая ошибки, когда сложения, вычитания и умножения приводят к переполнению, производя значения, которые не являются корректными. В этом blog post Дэн Лу анализирует это и обнаруживает, что вместо тривиальной стоимости, которую этот подход должен теоретически иметь, он в конечном итоге имеет значительную стоимость из-за того, что компиляторы (LLVM и GCC) не оптимизируют добавленные проверки переполнения. Если это улучшится в будущем, мы могли бы рассмотреть возможность использования проверенной целочисленной арифметики по умолчанию в Julia, но на данный момент нам придется смириться с возможностью переполнения.
Тем временем, безопасные для переполнения операции с целыми числами могут быть достигнуты с помощью внешних библиотек, таких как SaferIntegers.jl. Обратите внимание, что, как уже упоминалось ранее, использование этих библиотек значительно увеличивает время выполнения кода, использующего проверенные целочисленные типы. Однако для ограниченного использования это гораздо менее серьезная проблема, чем если бы это использовалось для всех операций с целыми числами. Вы можете следить за статусом обсуждения here.
What are the possible causes of an UndefVarError
during remote execution?
Как указывает ошибка, непосредственной причиной UndefVarError
на удаленном узле является то, что привязка с таким именем не существует. Давайте рассмотрим некоторые возможные причины.
julia> module Foo
foo() = remotecall_fetch(x->x, 2, "Hello")
end
julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `Foo` not defined in `Main`
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 in `Main.Foo`
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 in `Main`
[...]
julia> anon_bar = ()->1
(::#21) (generic function with 1 method)
julia> remotecall_fetch(anon_bar, 2)
1
Troubleshooting "method not matched": parametric type invariance and MethodError
s
Why doesn't it work to declare foo(bar::Vector{Real}) = 42
and then call foo([1])
?
Как вы увидите, если попробуете это, результатом будет MethodError
:
julia> foo(x::Vector{Real}) = 42
foo (generic function with 1 method)
julia> foo([1])
ERROR: MethodError: no method matching foo(::Vector{Int64})
The function `foo` exists, but no method is defined for this combination of argument types.
Closest candidates are:
foo(!Matched::Vector{Real})
@ Main none:1
Stacktrace:
[...]
Это связано с тем, что Vector{Real}
не является суперклассом Vector{Int}
! Вы можете решить эту проблему с помощью чего-то вроде foo(bar::Vector{T}) where {T<:Real}
(или короткой формы foo(bar::Vector{<:Real})
, если статический параметр T
не нужен в теле функции). T
— это подстановочный знак: вы сначала указываете, что он должен быть подтипом Real, а затем указываете, что функция принимает вектор с элементами этого типа.
Эта же проблема касается любого составного типа Comp
, а не только Vector
. Если у Comp
есть параметр, объявленный типа Y
, то другой тип Comp2
с параметром типа X<:Y
не является подтипом Comp
. Это инвариантность типов (в отличие от Tuple, который является ковариантным в своих параметрах). См. Parametric Composite Types для более подробного объяснения этих вопросов.
Why does Julia use *
for string concatenation? Why not +
or something else?
main argument против +
заключается в том, что конкатенация строк не является коммутативной, в то время как +
обычно используется как коммутативный оператор. Хотя сообщество Julia признает, что в других языках используются разные операторы и *
может быть незнаком некоторым пользователям, он передает определенные алгебраические свойства.
Обратите внимание, что вы также можете использовать string(...)
для конкатенации строк (и других значений, преобразованных в строки); аналогично, repeat
может быть использован вместо ^
для повторения строк. interpolation syntax также полезен для построения строк.
Packages and Modules
What is the difference between "using" and "import"?
Существует несколько различий между using
и import
(см. Modules section), но есть одно важное различие, которое может показаться неинтуитивным на первый взгляд, и на поверхности (т.е. с точки зрения синтаксиса) оно может показаться очень незначительным. При загрузке модулей с помощью using
вам нужно сказать function Foo.bar(...
, чтобы расширить функцию bar
модуля Foo
новым методом, но с import Foo.bar
вам нужно только сказать function bar(...
, и это автоматически расширяет функцию bar
модуля Foo
.
Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширить функцию, о существовании которой не знали, потому что это может легко вызвать ошибку. Это наиболее вероятно произойдет с методом, который принимает общий тип, такой как строка или целое число, потому что как вы, так и другой модуль могут определить метод для обработки такого общего типа. Если вы используете import
, то вы замените реализацию другого модуля bar(s::AbstractString)
вашей новой реализацией, которая может легко делать что-то совершенно другое (и сломать все/многие будущие использования других функций в модуле Foo, которые зависят от вызова bar).
Nothingness and missing values
How does "null", "nothingness" or "missingness" work in Julia?
В отличие от многих языков (например, C и Java), объекты Julia по умолчанию не могут быть "null". Когда ссылка (переменная, поле объекта или элемент массива) не инициализирована, доступ к ней немедленно вызовет ошибку. Эта ситуация может быть обнаружена с помощью функций isdefined
или isassigned
.
Некоторые функции используются только для их побочных эффектов и не нуждаются в возврате значения. В этих случаях принято возвращать значение nothing
, которое является единственным объектом типа Nothing
. Это обычный тип без полей; в нем нет ничего особенного, кроме этой конвенции, и то, что REPL ничего не печатает для него. Некоторые языковые конструкции, которые в противном случае не имели бы значения, также возвращают nothing
, например if false; end
.
Для ситуаций, когда значение x
типа T
существует только иногда, можно использовать тип Union{T, Nothing}
для аргументов функций, полей объектов и типов элементов массивов в качестве эквивалента Nullable
, Option
or 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{}
(пустой объединенный тип), является типом без значений и подтипов (кроме самого себя). Обычно вам не потребуется использовать этот тип.
Memory
Why does x += y
allocate memory when x
and y
are arrays?
В Julia x += y
заменяется на этапе понижения на x = x + y
. Для массивов это имеет следствие, что вместо хранения результата в том же месте в памяти, что и x
, выделяется новый массив для хранения результата. Если вы предпочитаете изменять x
, используйте x .+= y
, чтобы обновить каждый элемент индивидуально.
Хотя это поведение может удивить некоторых, выбор является преднамеренным. Основная причина заключается в наличии неизменяемых объектов в 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
.
Поскольку поддержка обобщенного программирования считается более важной, чем потенциальные оптимизации производительности, которые можно достичь другими способами (например, с помощью широковещательной передачи или явных циклов), операторы, такие как +=
и *=
, работают путем переназначения новых значений.
Asynchronous IO and concurrent synchronous writes
Why do concurrent writes to the same stream result in inter-mixed output?
Хотя 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
Arrays
What are the differences between zero-dimensional arrays and scalars?
Нулевымерные массивы — это массивы формы 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]
и т.д.; см. Omitted and extra indices.
Важно также понимать различия с обычными скалярами. Скаляры не являются изменяемыми контейнерами (хотя они и итерируемы и определяют такие вещи, как length
, getindex
, например, 1[] == 1
). В частности, если x = 0.0
определен как скаляр, то попытка изменить его значение через x[] = 1.0
будет ошибкой. Скаляр x
можно преобразовать в нульмерный массив, содержащий его, с помощью fill(x)
, и наоборот, нульмерный массив a
можно преобразовать в содержащийся скаляр с помощью a[]
. Еще одно различие заключается в том, что скаляр может участвовать в операциях линейной алгебры, таких как 2 * rand(2,2)
, но аналогичная операция с нульмерным массивом fill(2) * rand(2,2)
является ошибкой.
Why are my Julia benchmarks for linear algebra operations different from other languages?
Вы можете обнаружить, что простые бенчмарки строительных блоков линейной алгебры, таких как
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, так как он не является открытым исходным кодом.
Computing cluster
How do I manage precompilation caches in distributed file systems?
Когда вы используете Julia в высокопроизводительных вычислительных (HPC) центрах с общими файловыми системами, рекомендуется использовать общий депо (через переменную окружения JULIA_DEPOT_PATH
). Начиная с версии Julia v1.10, несколько процессов Julia на функционально схожих рабочих узлах и использующих один и тот же депо будут координироваться через блокировки pidfile, чтобы тратить усилия на предварительную компиляцию только в одном процессе, в то время как другие будут ждать. Процесс предварительной компиляции будет указывать, когда процесс выполняет предварительную компиляцию или ждет другой, который выполняет предварительную компиляцию. Если не интерактивно, сообщения будут через @debug
.
Однако, из-за кэширования двоичного кода, отклонение кэша с версии v1.9 стало более строгим, и пользователям может потребоваться правильно установить переменную окружения JULIA_CPU_TARGET
, чтобы получить единый кэш, который можно использовать в всей среде HPC.
Julia Releases
Do I want to use the Stable, LTS, or nightly version of Julia?
Стабильная версия Julia — это последняя выпущенная версия Julia, которую большинство людей захочет использовать. Она имеет последние функции, включая улучшенную производительность. Стабильная версия Julia имеет версию в соответствии с SemVer как v1.x.y. Новая минорная версия Julia, соответствующая новой стабильной версии, выпускается примерно каждые 4-5 месяцев после нескольких недель тестирования в качестве кандидат на выпуск. В отличие от LTS версии, стабильная версия обычно не получает исправлений ошибок после выхода другой стабильной версии Julia. Тем не менее, обновление до следующей стабильной версии всегда будет возможным, так как каждая версия Julia v1.x будет продолжать выполнять код, написанный для более ранних версий.
Вы можете предпочесть версию LTS (Long Term Support) Julia, если ищете очень стабильную кодовую базу. Текущая версия LTS Julia имеет версию v1.6.x согласно SemVer; эта ветка будет продолжать получать исправления ошибок до тех пор, пока не будет выбрана новая ветка LTS, после чего серия v1.6.x больше не будет получать регулярные исправления ошибок, и всем, кроме самых консервативных пользователей, будет рекомендовано обновиться до новой серии версий LTS. В качестве разработчика пакетов вы можете предпочесть разрабатывать для версии LTS, чтобы максимизировать количество пользователей, которые могут использовать ваш пакет. Согласно SemVer, код, написанный для v1.0, будет продолжать работать для всех будущих версий LTS и Stable. В общем, даже если вы нацелены на LTS, можно разрабатывать и запускать код в последней версии Stable, чтобы воспользоваться улучшенной производительностью; при условии, что вы избегаете использования новых функций (таких как добавленные функции библиотек или новые методы).
Вы можете предпочесть ночную версию Julia, если хотите воспользоваться последними обновлениями языка и не против, если версия, доступная сегодня, иногда не работает. Как следует из названия, релизы ночной версии выходят примерно каждую ночь (в зависимости от стабильности инфраструктуры сборки). В общем, ночные релизы довольно безопасны для использования — ваш код не загорится. Однако могут быть случайные регрессии и или проблемы, которые не будут обнаружены до более тщательного тестирования перед релизом. Вы можете захотеть протестировать ночную версию, чтобы убедиться, что такие регрессии, которые влияют на ваш случай использования, будут обнаружены до выпуска.
Наконец, вы также можете рассмотреть возможность сборки Julia из исходного кода для себя. Этот вариант в основном предназначен для тех людей, которые чувствуют себя комфортно в командной строке или заинтересованы в обучении. Если это описывает вас, вам также может быть интересно прочитать наш guidelines for contributing.
Ссылки на каждый из этих типов загрузки можно найти на странице загрузки по адресу https://julialang.org/downloads/. Обратите внимание, что не все версии Julia доступны для всех платформ.
How can I transfer the list of installed packages after updating my version of Julia?
Каждая минорная версия Julia имеет свой собственный стандартный environment. В результате, при установке новой минорной версии 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
.