Часто задаваемые вопросы
Общие
Является ли 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?
Функциональность Julia Base и стандартной библиотеки, описанная в документации, которая не помечена как неустойчивая (например, экспериментальная и внутренняя), покрывается SemVer. Функции, типы и константы не являются частью публичного API, если они не включены в документацию, даже если у них есть docstrings.
Существует полезная недокументированная функция/тип/константа. Могу ли я ее использовать?
Обновление Julia может сломать ваш код, если вы используете непубличный API. Если код самодостаточен, возможно, стоит скопировать его в свой проект. Если вы хотите полагаться на сложный непубличный API, особенно при использовании его из стабильного пакета, неплохо было бы открыть запрос на issue или pull request, чтобы начать обсуждение по поводу преобразования его в публичный API. Однако мы не препятствуем попыткам создания пакетов, которые экспонируют стабильные публичные интерфейсы, опираясь на непубличные детали реализации Julia и буферизации различий между различными версиями Julia.
Документация недостаточно точна. Могу ли я полагаться на существующее поведение?
Пожалуйста, откройте запрос на issue или pull request, чтобы начать обсуждение по поводу преобразования существующего поведения в публичный API.
Сессии и REPL
Как удалить объект в памяти?
В Julia нет аналога функции MATLAB clear; как только имя определено в сессии Julia (технически, в модуле Main), оно всегда присутствует.
Если вас беспокоит использование памяти, вы всегда можете заменить объекты объектами, которые потребляют меньше памяти. Например, если A — это массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = nothing. Память будет освобождена в следующий раз, когда будет запущена сборка мусора; вы можете заставить это произойти с помощью GC.gc(). Более того, попытка использовать A, скорее всего, приведет к ошибке, поскольку большинство методов не определены для типа Nothing.
Как изменить объявление типа в моей сессии?
Возможно, вы определили тип, а затем поняли, что вам нужно добавить новое поле. Если вы попытаетесь сделать это в REPL, вы получите ошибку:
ERROR: invalid redefinition of constant MyType
Типы в модуле Main не могут быть переопределены.
Хотя это может быть неудобно при разработке нового кода, существует отличное решение. Модули можно заменить, переопределяя их, и поэтому, если вы обернете весь новый код в модуль, вы сможете переопределить типы и константы. Вы не можете импортировать имена типов в Main и ожидать, что сможете их переопределить там, но вы можете использовать имя модуля для разрешения области действия. Другими словами, во время разработки вы можете использовать рабочий процесс примерно такой:
include("mynewcode.jl") # this defines a module MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Got an error. Change something in "mynewcode.jl"
include("mynewcode.jl") # reload the module
obj1 = MyModule.ObjConstructor(a, b) # old objects are no longer valid, must reconstruct
obj2 = MyModule.somefunction(obj1) # this time it worked!
obj3 = MyModule.someotherfunction(obj2, c)
...
Сценарии
Как проверить, что текущий файл запускается как основной сценарий?
При запуске файла в качестве основного сценария с помощью julia file.jl можно активировать дополнительную функциональность, например, обработку аргументов командной строки. Способ определения того, что файл запускается таким образом, заключается в проверке, является ли abspath(PROGRAM_FILE) == @__FILE__ true.
Однако рекомендуется не создавать файлы, которые одновременно являются скриптом и импортируемой библиотекой. Если вам нужна функциональность, доступная как в качестве библиотеки, так и в качестве скрипта, лучше написать её в виде библиотеки, а затем импортировать функциональность в отдельный скрипт.
Как перехватить CTRL-C в скрипте?
Запуск скрипта Julia с использованием julia file.jl не генерирует InterruptException, когда вы пытаетесь завершить его с помощью CTRL-C (SIGINT). Чтобы выполнить определенный код перед завершением скрипта Julia, который может быть или не быть вызван CTRL-C, используйте atexit. В качестве альтернативы, вы можете использовать julia -e 'include(popfirst!(ARGS))' file.jl для выполнения скрипта, при этом имея возможность перехватить InterruptException в блоке try. Обратите внимание, что в этом случае 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 не поддерживает * или конвейеры для управления внешними программами?
Функция Julia run запускает внешние программы непосредственно, без вызова оболочки операционной системы (в отличие от функции system("...") в других языках, таких как Python, R или C). Это означает, что run не выполняет расширение подстановочных знаков * («globbing»), а также не интерпретирует конвейеры оболочки, такие как | или >.
Вы все равно можете использовать globbing и конвейеры с помощью функций Julia. Например, встроенная функция pipeline позволяет объединять внешние программы и файлы, подобно конвейерам оболочки, а пакет Glob.jl реализует совместимые с POSIX globbing.
Конечно, вы можете запускать программы через оболочку, явно передав оболочку и строку команды в run, например, run(`sh -c "ls > files.txt"`) для использования оболочки Bourne, но в целом следует отдавать предпочтение чистому скриптингу 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
Stacktrace:
[...]
Закрытие x->x ссылается на Foo, и поскольку Foo недоступно на узле 2, генерируется ошибка UndefVarError.
Глобальные переменные, относящиеся к модулям, отличным от Main, не сериализуются по значению на удалённый узел. Отправляется только ссылка. Функции, создающие глобальные привязки (кроме тех, что под Main ), могут привести к возникновению ошибки UndefVarError позже.
julia> @everywhere module Foo
function foo()
global gvar = "Hello"
remotecall_fetch(()->gvar, 2)
end
end
julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `gvar` not defined
Stacktrace:
[...]
В приведённом примере @everywhere module Foo определил Foo на всех узлах. Однако вызов Foo.foo() создал новую глобальную привязку gvar на локальном узле, но её не нашёл узел 2, что привело к ошибке UndefVarError.
Обратите внимание, что это не относится к глобальным переменным, созданным в модуле Main. Глобальные переменные в модуле Main сериализуются, и новые связи, созданные в Main на удалённом узле.
julia> gvar_self = "Node1" "Node1" julia> remotecall_fetch(()->gvar_self, 2) "Node1" julia> remotecall_fetch(varinfo, 2) name size summary ––––––––– –––––––– ––––––– Base Module Core Module Main Module gvar_self 13 bytes String
Это не относится к объявлениям function или struct. Однако, анонимные функции, связанные с глобальными переменными, сериализуются, как показано ниже.
julia> bar() = 1 bar (generic function with 1 method) julia> remotecall_fetch(bar, 2) ERROR: On worker 2: UndefVarError: `#bar` not defined [...] julia> anon_bar = ()->1 (::#21) (generic function with 1 method) julia> remotecall_fetch(anon_bar, 2) 1
Устранение неполадок "метод не найден": параметрическая инвариантность типов и 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})
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, который является параметрически ковариантным). Для получения более подробной информации о параметрических составных типах см. Параметрические составные типы.
Почему 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, зависящих от вызова bar).
Ничтожность и пропущенные значения
Как работают "null", "ничтожность" или "пропущенность" в Julia?
В отличие от многих языков (например, C и Java), объекты Julia по умолчанию не могут быть «null». Когда ссылка (переменная, поле объекта или элемент массива) не инициализирована, доступ к ней сразу вызывает ошибку. Такую ситуацию можно обнаружить с помощью функций isdefined или isassigned.
Некоторые функции используются только для побочных эффектов и не требуют возвращаемого значения. В этих случаях принято возвращать значение nothing, которое представляет собой просто одиночный объект типа Nothing. Это обычный тип без полей; в нём нет ничего особенного, кроме этой соглашения, и что REPL ничего не выводит для него. Некоторые конструкции языка, которые в противном случае не имели бы значения, также возвращают nothing, например if false; end.
Для ситуаций, когда значение x типа T существует только иногда, можно использовать тип Union{T, Nothing} для аргументов функций, полей объектов и типов элементов массивов, как аналог Nullable, Option или Maybe в других языках. Если само значение может быть nothing (в частности, когда T равно Any ), тип Union{Some{T}, Nothing} более подходящий, так как x == nothing тогда указывает на отсутствие значения, а x == Some(nothing) указывает на присутствие значения, равного nothing Функция something позволяет распаковывать объекты Some и использовать значение по умолчанию вместо аргументов nothing. Обратите внимание, что компилятор может генерировать эффективный код при работе с аргументами или полями Union{T, Nothing}.
Для представления отсутствующих данных в статистическом смысле (NA в R или NULL в SQL) используйте объект missing. Для получения более подробной информации см. раздел Missing Values.
В некоторых языках пустой кортеж (()) считается канонической формой ничтожности. Однако в Julia лучше рассматривать его как обычный кортеж, содержащий ноль значений.
Пустой (или «нижний») тип, записанный как Union{} (пустой тип объединения), — это тип без значений и подтипов (кроме самого себя). Вам, как правило, не нужно будет использовать этот тип.
Память
Почему x += y выделяет память, когда x и y являются массивами?
В Julia x += y заменяется на x = x + y во время понижения. Для массивов это означает, что вместо хранения результата в том же месте памяти, что и x, создаётся новый массив для хранения результата. Если вы предпочитаете изменять 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 в высокопроизводительных вычислительных (HPC) установках с общими файловыми системами рекомендуется использовать общий склад (через переменную окружения JULIA_DEPOT_PATH). С Julia v1.10 несколько процессов Julia на функционально схожих узлах и использующих один и тот же склад будут координироваться с помощью блокировок pidfile, чтобы только один процесс тратил усилия на предварительную компиляцию, а другие ждали. Процесс предварительной компиляции будет указывать, когда процесс выполняет предварительную компиляцию или ожидает другой процесс, который выполняет предварительную компиляцию. Если не интерактивно, сообщения будут через @debug.
Однако из-за кэширования двоичного кода отказ кэша с v1.9 стал более строгим, и пользователям может потребоваться правильно установить переменную окружения JULIA_CPU_TARGET, чтобы получить единый кэш, пригодный для использования во всей HPC-среде.
Выпуски Julia
Какую версию Julia использовать: Stable, LTS или nightly?
Версия Stable — это последняя выпущенная версия Julia, которую большинство пользователей захотят использовать. Она содержит новейшие функции, включая улучшенную производительность. Версия Stable Julia имеет версионирование согласно SemVer как v1.x.y. Новый мажорный выпуск Julia, соответствующий новой версии Stable, происходит примерно каждые 4–5 месяцев после нескольких недель тестирования в качестве кандидата на выпуск. В отличие от версии LTS, версия Stable обычно не получает исправления ошибок после выпуска другой версии Stable Julia. Однако переход к следующему выпуску Stable всегда возможен, так как каждый выпуск Julia v1.x будет продолжать выполнять код, написанный для предыдущих версий.
Вы можете предпочесть версию LTS (Long Term Support) Julia, если вам нужна очень стабильная кодовая база. Текущая LTS-версия Julia имеет версионирование согласно SemVer как v1.6.x; эта ветка будет продолжать получать исправления ошибок до выбора новой LTS-ветки, после чего серия v1.6.x больше не будет регулярно получать исправления ошибок, и большинству пользователей будет рекомендовано обновить до новой серии LTS-версий. Как разработчику пакетов, вы можете предпочесть разработку для LTS-версии, чтобы максимизировать количество пользователей, которые могут использовать ваш пакет. В соответствии с SemVer, код, написанный для v1.0, будет продолжать работать для всех будущих LTS- и Stable-версий. В целом, даже если вы ориентируетесь на LTS, вы можете разрабатывать и запускать код в последней Stable-версии, чтобы воспользоваться улучшенной производительностью; при условии, что вы избегаете использования новых функций (таких как добавленные функции библиотеки или новые методы).
Вы можете предпочесть версию nightly Julia, если хотите воспользоваться последними обновлениями языка и не против, если доступная сегодня версия иногда не работает. Как следует из названия, выпуски в nightly-версию происходят примерно каждую ночь (в зависимости от стабильности инфраструктуры сборки). В целом, nightly-выпуски достаточно безопасны для использования — ваш код не загорится. Однако могут быть случайные регрессии или проблемы, которые не будут обнаружены до более тщательного предварительного тестирования. Вы можете протестировать nightly-версию, чтобы убедиться, что такие регрессии, которые влияют на ваш случай использования, будут обнаружены до выпуска.
Наконец, вы также можете рассмотреть возможность сборки 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.10/manual/faq/