Часто задаваемые вопросы
Общие
Является ли 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++ и другими языками.
Публичный API
Как Julia определяет свой публичный API?
Публичный API Julia — это поведение, описанное в документации общедоступных символов из Base и стандартных библиотек. Функции, типы и константы не являются частью публичного API, если они не общедоступны, даже если у них есть строки документации или они описаны в документации. Кроме того, частью публичного API является только задокументированное поведение общедоступных символов. Незадокументированное поведение общедоступных символов — внутреннее.
Общедоступные символы помечены либо public foo либо export foo.
Другими словами:
- Задокументированное поведение общедоступных символов является частью публичного API.
- Незадокументированное поведение общедоступных символов не является частью публичного API.
- Задокументированное поведение закрытых символов не является частью публичного API.
- Незадокументированное поведение закрытых символов не является частью публичного API.
Полный список общедоступных символов из модуля можно получить с помощью names(MyModule).
Авторам пакетов рекомендуется аналогично определять свой публичный API.
Любой элемент публичного API Julia регулируется SemVer и поэтому не будет удалён или подвержен существенным изменениям до Julia 2.0.
Существует полезная недокументированная функция/тип/константа. Могу ли я её использовать?
Обновление Julia может привести к ошибкам в вашем коде, если вы используете непубличный API. Если код автономный, то, возможно, стоит скопировать его в свой проект. Если вы хотите полагаться на сложный непубличный API, особенно при его использовании из стабильного пакета, рекомендуется открыть запрос на ошибку или запрос на добавление, чтобы начать обсуждение о превращении его в публичный API. Однако мы не отговариваем от попыток создавать пакеты, которые экспонируют стабильные публичные интерфейсы, полагаясь на непубличные детали реализации Julia и буферируя различия между различными версиями Julia.
Документация недостаточно точна. Могу ли я полагаться на существующее поведение?
Пожалуйста, откройте запрос на ошибку или запрос на добавление, чтобы начать обсуждение о превращении существующего поведения в публичный API.
Сессии и REPL
Как удалить объект в памяти?
Julia не имеет аналога функции MATLAB's 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. Обратите внимание, что с этой стратегией PROGRAM_FILE не будет установлено.
Как передать параметры к julia с использованием #!/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).
Почему run не поддерживает * или каналы для управления внешними программами?
Функция run Julia запускает внешние программы непосредственно, без вызова оболочки операционной системы (в отличие от функции system("...") в других языках, таких как Python, R или C). Это означает, что run не выполняет расширение подстановок * («глоббинга»), а также не интерпретирует командные цепочки, такие как | или >.
Тем не менее, вы по-прежнему можете использовать глобальное расширение и командные цепочки с помощью функций Julia. Например, встроенная функция pipeline позволяет вам объединять внешние программы и файлы, подобно командным цепочкам, а пакет Glob.jl реализует совместимое с POSIX глобальное расширение.
Конечно, вы можете запускать программы через оболочку, явно передав оболочку и строку команды функции run, например, run(`sh -c "ls > files.txt"`) для использования оболочки Bourne в Unix, но в целом следует отдавать предпочтение чисто скриптовым программам Julia, таким как run(pipeline(`ls`, "files.txt")). Причина, по которой мы по умолчанию избегаем использования оболочки, заключается в том, что запуск процессов через оболочку медленный, хрупкий при цитировании специальных символов, имеет плохую обработку ошибок и проблематичен для портируемости. (Разработчики Python пришли к аналогичному выводу).
Переменные и присваивания
Почему я получаю UndefVarError из простого цикла?
У вас может быть что-то вроде:
x = 0
while x < 10
x += 1
end
и вы заметите, что это работает в интерактивной среде (например, в Julia REPL), но возвращает UndefVarError: `x` not defined при попытке запустить его в скрипте или другом файле. Причина в том, что Julia, как правило, требует явного присваивания глобальным переменным в локальном пространстве имен.
Здесь x — это глобальная переменная, while определяет локальное пространство имен, а x += 1 — это присваивание глобальной переменной в этом локальном пространстве имен.
Как упоминалось выше, Julia (версия 1.5 или более поздняя) позволяет опустить ключевое слово global для кода в REPL (и во многих других интерактивных средах) для упрощения изучения (например, копирования и вставки кода из функции для интерактивного запуска). Однако при работе с кодом в файлах Julia требует более дисциплинированного подхода к глобальным переменным. У вас есть по крайней мере три варианта:
- Поместите код в функцию (так что
xбудет локальной переменной в функции). В целом, хорошее проектирование программного обеспечения подразумевает использование функций вместо глобальных скриптов (поищите в Интернете «почему глобальные переменные плохи», чтобы увидеть множество объяснений). В Julia глобальные переменные также медленные. - Оборатите код в блок
let. (Это делаетxлокальной переменной в оператореlet ... end, снова устраняя необходимость вglobal). - Явно пометьте
xкакglobalвнутри локального пространства имен перед присваиванием, например, напишитеglobal x += 1.
Более подробное объяснение можно найти в разделе руководства о мягком пространстве имен.
Функции
Я передал аргумент 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 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
Типы, объявления типов и конструкторы
Что означает «стабильность типа»?
Это означает, что тип результата предсказуем по типам входных данных. В частности, это означает, что тип результата не может меняться в зависимости от значений входных данных. Следующий код не является типостабильным:
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 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. Можно было бы написать функцию 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 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
Устранение неполадок "метод не найден": параметрическая инвариантность типов и MethodError
Почему не работает объявление foo(bar::Vector{Real}) = 42 и последующий вызов 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 . Это инвариантность типов (в отличие от кортежа, который является ковариативным по параметрам). См. Параметрические составные типы для получения более подробной информации об этом.
Почему Julia использует * для конкатенации строк? Почему не + или что-то другое?
Основной аргумент против + заключается в том, что конкатенация строк не коммутативна, тогда как + обычно используется как коммутативный оператор. Хотя сообщество Julia признаёт, что другие языки используют разные операторы и * может быть непривычным для некоторых пользователей, это отражает определённые алгебраические свойства.
Обратите внимание, что вы также можете использовать string(...) для конкатенации строк (и других значений, преобразованных в строки); аналогично, repeat можно использовать вместо ^ для повторения строк. Синтаксис интерполяции строк также полезен для построения строк.
Пакеты и модули
В чём разница между "using" и "import"?
Существует несколько различий между using и import (см. раздел Модули), но есть важное различие, которое может показаться не очевидным на первый взгляд, а на поверхности (т.е. с точки зрения синтаксиса) оно может показаться очень незначительным. При загрузке модулей с помощью using, вам нужно сказать function Foo.bar(... для расширения функции модуля Foo новым методом, но с import Foo.bar, вам нужно сказать только function bar(... и она автоматически расширит функцию модуля Foo новым методом bar.
Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширять функцию, о существовании которой вы не знаете, так как это может легко привести к ошибке. Это наиболее вероятно произойдёт с методом, который принимает общий тип, такой как строка или целое число, поскольку и вы, и другой модуль можете определить метод для обработки такого общего типа. Если вы используете import, вы замените реализацию bar(s::AbstractString) другого модуля вашей новой реализацией, которая может легко сделать что-то совершенно другое (и сломать все/многие будущие использования других функций в модуле Foo, которые зависят от вызова бар).
Пустота и отсутствующие значения
Как работают "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, создаётся новый массив для хранения результата. Если вы предпочитаете изменять 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.
Поскольку поддержка обобщённого программирования считается более важной, чем потенциальные оптимизации производительности, которые могут быть достигнуты другими средствами (например, с помощью векторизации или явных циклов), операторы, такие как += и *= работают путём перепривязки новых значений.
Асинхронный ввод-вывод и одновременные синхронные записи
Почему одновременные записи в один и тот же поток приводят к перемешанному выводу?
Хотя API потоковой ввод-вывод является синхронным, его внутренняя реализация полностью асинхронна.
Рассмотрим вывод, полученный из следующего:
julia> @sync for i in 1:3
@async write(stdout, string(i), " Foo ", " Bar ")
end
123 Foo Foo Foo Bar Bar Bar
Это происходит потому, что, хотя вызов write синхронный, запись каждого аргумента уступает другие задачи во время ожидания завершения этой части ввода-вывода.
print и println «блокируют» поток во время вызова. Вследствие этого изменение write на println в приведенном выше примере приводит к:
julia> @sync for i in 1:3
@async println(stdout, string(i), " Foo ", " Bar ")
end
1 Foo Bar
2 Foo Bar
3 Foo Bar
Вы можете заблокировать свои записи с помощью ReentrantLock следующим образом:
julia> l = ReentrantLock();
julia> @sync for i in 1:3
@async begin
lock(l)
try
write(stdout, string(i), " Foo ", " Bar ")
finally
unlock(l)
end
end
end
1 Foo Bar 2 Foo Bar 3 Foo Bar
Массивы
В чем разница между массивами нулевой размерности и скалярами?
Массивы нулевой размерности — это массивы вида Array{T,0}. Они ведут себя подобно скалярам, но существуют важные различия. Они заслуживают особого упоминания, поскольку являются особым случаем, который имеет логический смысл в свете общего определения массивов, но может быть немного неинтуитивным на первый взгляд. Следующая строка определяет массив нулевой размерности:
julia> A = zeros()
0-dimensional Array{Float64,0}:
0.0
В этом примере A — это изменяемый контейнер, содержащий один элемент, который можно установить с помощью A[] = 1.0 и получить с помощью A[]. Все массивы нулевой размерности имеют одинаковый размер (size(A) == ()) и длину (length(A) == 1). В частности, массивы нулевой размерности не являются пустыми. Если вам это кажется неинтуитивным, вот несколько идей, которые могут помочь понять определение массивов в Julia.
- Массивы нулевой размерности — это «точка» для «линии» вектора и «плоскости» матрицы. Так же как у линии нет площади (но она все равно представляет набор вещей), у точки нет длины или каких-либо измерений вообще (но она все равно представляет собой вещь).
- Мы определим
prod(())как 1, а общее количество элементов в массиве — это произведение размеров. Размер массива нулевой размерности —(), а его длина —1. - Массивы нулевой размерности изначально не имеют измерений, в которых можно индексировать — они просто
A[]. Мы можем применить то же правило «приводящего к единице» к ним, что и ко всем другим размерностям массивов, поэтому вы действительно можете индексировать их какA[1],A[1,1], и т. д.; см. Пропущенные и дополнительные индексы.
Также важно понять различия с обычными скалярами. Скаляры не являются изменяемыми контейнерами (хотя они итерируемы и определяют такие вещи, как length, getindex, например 1[] == 1). В частности, если x = 0.0 определен как скаляр, попытка изменить его значение с помощью x[] = 1.0 является ошибкой. Скаляр x можно преобразовать в массив нулевой размерности, содержащий его, с помощью fill(x), и наоборот, массив нулевой размерности a можно преобразовать в содержащийся в нем скаляр с помощью a[]. Еще одно различие заключается в том, что скаляр может участвовать в операциях линейной алгебры, таких как 2 * rand(2,2), но аналогичная операция с массивом нулевой размерности fill(2) * rand(2,2) — это ошибка.
Почему мои бенчмарки Julia для операций линейной алгебры отличаются от других языков?
Вы можете обнаружить, что простые бенчмарки базовых блоков линейной алгебры, таких как
using BenchmarkTools A = randn(1000, 1000) B = randn(1000, 1000) @btime $A \ $B @btime $A * $B
могут отличаться при сравнении с другими языками, такими как Matlab или R.
Поскольку операции такого рода являются очень тонкими оболочками над соответствующими функциями BLAS, причиной расхождения, скорее всего, является
библиотека BLAS, используемая каждым языком,
количество потоков.
Julia компилирует и использует свою собственную копию OpenBLAS, с потоками, в настоящее время ограниченными 8 (или количеством ваших ядер).
Изменение настроек OpenBLAS или компиляция Julia с другой библиотекой BLAS, например Intel MKL, может улучшить производительность. Вы можете использовать MKL.jl, пакет, который заставляет линейную алгебру Julia использовать Intel MKL BLAS и LAPACK вместо OpenBLAS, или искать в форуме обсуждений рекомендации по ручному настройке этого.
Обратите внимание, что Intel MKL не может быть включен в состав Julia, поскольку он не является открытым исходным кодом.
Вычислительный кластер
Как управлять кэшами предварительной компиляции в распределенных файловых системах?
При использовании Julia в высокопроизводительных вычислительных (ВВК) средах с общими файловыми системами рекомендуется использовать общий хранилище (через переменную среды JULIA_DEPOT_PATH). С версии Julia 1.10 несколько процессов Julia на функционально схожих рабочих узлах и использующих одно и то же хранилище будут координировать действия посредством блокировок файлов pid, чтобы потратить усилия на предварительную компиляцию только в одном процессе, а остальные будут ждать. Процесс предварительной компиляции будет указывать, когда процесс выполняет предварительную компиляцию или ожидает другого, который выполняет предварительную компиляцию. В неинтерактивном режиме сообщения передаются через @debug.
Однако из-за кэширования двоичного кода отказ от кэша с версии 1.9 более строгий, и пользователям может потребоваться установить переменную среды JULIA_CPU_TARGET соответствующим образом, чтобы получить единый кэш, который можно использовать во всей среде ВВК.
Выпуски Julia
Какую версию Julia (Stable, LTS или nightly) мне использовать?
Версия Julia Stable — это последняя выпущенная версия Julia, которую большинство людей захотят использовать. Она имеет самые последние функции, включая улучшенную производительность. Версия Julia Stable имеет версионирование в соответствии со спецификацией SemVer как v1.x.y. Новый мажорный выпуск Julia, соответствующий новой версии Stable, выпускается примерно каждые 4-5 месяцев после нескольких недель тестирования в качестве кандидата в релиз. В отличие от версии LTS, версия Stable обычно не получает исправления ошибок после выпуска другой версии Stable Julia. Однако обновление до следующего стабильного выпуска всегда возможно, так как каждый выпуск Julia v1.x будет продолжать работать с кодом, написанным для более ранних версий.
Вы можете предпочесть версию LTS (Long Term Support) Julia, если вам нужна очень стабильная база кода. Текущая версия Julia LTS имеет версионирование в соответствии со спецификацией SemVer как v1.6.x; эта ветка будет продолжать получать исправления ошибок до тех пор, пока не будет выбрана новая ветка LTS, в этот момент серия v1.6.x больше не будет получать регулярные исправления ошибок, и всем, кроме самых консервативных пользователей, будет рекомендовано обновить до новой серии LTS. В качестве разработчика пакетов вы можете предпочесть разработку для версии LTS, чтобы максимизировать количество пользователей, которые могут использовать ваш пакет. Согласно SemVer, код, написанный для v1.0, продолжит работать во всех будущих версиях LTS и Stable. В общем случае, даже если вы нацеливаетесь на LTS, вы можете разрабатывать и запускать код в последней версии Stable, чтобы воспользоваться улучшенной производительностью; при условии, что вы избегаете использования новых функций (таких как добавленные функции библиотек или новые методы).
Вы можете предпочесть ночную версию 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/manual/faq/