Часто задаваемые вопросы
Сеансы и REPL
Как удалить объект в памяти?
В Julia нет аналога функции MATLAB clear; после определения имени в сеансе Julia (технически, в модуле Main) оно всегда присутствует.
Если вас беспокоит использование памяти, вы всегда можете заменить объекты на объекты, которые потребляют меньше памяти. Например, если A — это массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = 0. Память будет освобождена при следующем запуске сборщика мусора; вы можете принудительно вызвать это с помощью gc().
Как изменить объявление типа/неизменяемого типа в моем сеансе?
Возможно, вы определили тип, а затем поняли, что вам нужно добавить новое поле. Если вы попытаетесь сделать это в 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)
...
Функции
Я передал аргумент x в функцию, изменил его внутри функции, но снаружи переменная x по-прежнему не изменилась. Почему?
Предположим, вы вызываете функцию так:
julia> x = 10
10
julia> function change_value!(y)
y = 17
end
change_value! (generic function with 1 method)
julia> change_value!(x)
17
julia> x # x is unchanged!
10
В Julia привязка переменной x не может быть изменена путем передачи x в качестве аргумента функции. При вызове change_value!(x) в приведенном выше примере y — это новая переменная, первоначально связанная со значением x, т. е. 10; затем y перепривязывается к константе 17, в то время как переменная x внешней области видимости остается неизменной.
Но вот на что стоит обратить внимание: предположим, что x привязано к объекту типа Array (или любого другого изменяемого типа). Изнутри функции вы не можете «отвязать» x от этого массива, но вы можете изменить его содержимое. Например:
julia> x = [1,2,3]
3-element Array{Int64,1}:
1
2
3
julia> function change_array!(A)
A[1] = 5
end
change_array! (generic function with 1 method)
julia> change_array!(x)
5
julia> x
3-element Array{Int64,1}:
5
2
3
Здесь мы создали функцию change_array!(), которая присваивает 5 первому элементу переданного массива (связанного с x в месте вызова и связанного с A внутри функции). Обратите внимание, что после вызова функции x по-прежнему привязана к тому же массиву, но содержимое этого массива изменилось: переменные A и x были разными связями, ссылающимися на один и тот же изменяемый объект Array.
Можно ли использовать using или import внутри функции?
Нет, у вас нет права иметь оператор using или import внутри функции. Если вам нужно импортировать модуль, но использовать его символы только внутри определенной функции или набора функций, у вас есть два варианта:
-
Используйте
import:import Foo function bar(...) ... refer to Foo symbols via Foo.baz ... endЭто загружает модуль
Fooи определяет переменнуюFoo, которая ссылается на модуль, но не импортирует какие-либо другие символы из модуля в текущее пространство имён. Вы обращаетесь к символамFooпо их квалифицированным именамFoo.barи т. д. -
Оберните свою функцию в модуль:
module Bar export bar using Foo function bar(...) ... refer to Foo.baz as simply baz .... end end using BarЭто импортирует все символы из
Foo, но только внутри модуляBar.
Что делает оператор ...?
Два использования оператора ...: slurping и splatting
Многие новые пользователи Julia находят использование оператора ... запутанным. Частично то, что делает оператор ... запутанным, заключается в том, что он означает две разные вещи в зависимости от контекста.
... объединяет много аргументов в один аргумент в определениях функций
В контексте определений функций оператор ... используется для объединения многих различных аргументов в один аргумент. Это использование ... для объединения многих различных аргументов в один аргумент называется slurping:
julia> function printargs(args...)
@printf("%s\n", typeof(args))
for (i, arg) in enumerate(args)
@printf("Arg %d = %s\n", i, arg)
end
end
printargs (generic function with 1 method)
julia> printargs(1, 2, 3)
(Int64,Int64,Int64)
Arg 1 = 1
Arg 2 = 2
Arg 3 = 3
Если бы Julia использовала более свободно символы ASCII, оператор slurping мог бы быть записан как <-... вместо ....
... разделяет один аргумент на многие разные аргументы в вызовах функций
В отличие от использования оператора ... для обозначения slurping многих различных аргументов в один аргумент при определении функции, оператор ... также используется для разделения одного аргумента функции на многие разные аргументы, когда он используется в контексте вызова функции. Это использование ... называется splatting:
julia> function threeargs(a, b, c)
@printf("a = %s::%s\n", a, typeof(a))
@printf("b = %s::%s\n", b, typeof(b))
@printf("c = %s::%s\n", c, typeof(c))
end
threeargs (generic function with 1 method)
julia> vec = [1, 2, 3]
3-element Array{Int64,1}:
1
2
3
julia> threeargs(vec...)
a = 1::Int64
b = 2::Int64
c = 3::Int64
Если бы Julia использовала более свободно символы ASCII, оператор splatting мог бы быть записан как ...-> вместо ....
Типы, объявления типов и конструкторы
Что означает «type-stable»?
Это означает, что тип результата предсказуем исходя из типов входных данных. В частности, это означает, что тип результата не может изменяться в зависимости от значений входных данных. Следующий код не является type-stable:
function unstable(flag::Bool)
if flag
return 1
else
return 1.0
end
end
Он возвращает либо Int, либо Float64, в зависимости от значения своего аргумента. Поскольку Julia не может предсказать тип результата в момент компиляции, любое вычисление, которое его использует, должно учитывать возможность обоих типов, что затрудняет генерацию быстрого машинного кода.
Почему Julia выдаёт DomainError для некоторых, казалось бы, осмысленных операций?
Некоторые операции математически осмысленны, но приводят к ошибкам:
julia> sqrt(-2.0) ERROR: DomainError in sqrt at math.jl:128 julia> 2^-5 ERROR: DomainError in power_by_squaring at intfuncs.jl:70 in ^ at intfuncs.jl:84
Это поведение является неудобным следствием требования type-stability. В случае sqrt() большинство пользователей хотят, чтобы sqrt(2.0) возвращало действительное число и не хотели бы, чтобы оно возвращало комплексное число 1.4142135623730951 + 0.0im. Можно было бы написать функцию sqrt(), которая переключалась бы на комплексное значение только при передаче отрицательного числа (что делает sqrt() в некоторых других языках), но тогда результат не был бы type-stable, и функция sqrt() имела бы плохую производительность.
В этих и других случаях вы можете получить желаемый результат, выбрав тип входных данных, который указывает вашу готовность принять тип выходных данных, в котором результат может быть представлен:
julia> sqrt(-2.0+0im) 0.0 + 1.4142135623730951im julia> 2.0^-5 0.03125
Почему Julia использует машинную арифметику для целых чисел?
Julia использует машинную арифметику для вычислений с целыми числами. Это означает, что диапазон значений Int ограничен и переполняется с обеих сторон, так что сложение, вычитание и умножение целых чисел может привести к переполнению или потере значимости, что приводит к некоторым результатам, которые поначалу могут быть неожиданными:
julia> typemax(Int) 9223372036854775807 julia> ans+1 -9223372036854775808 julia> -ans -9223372036854775808 julia> 2*ans 0
Очевидно, это сильно отличается от поведения математических целых чисел, и вы можете подумать, что для высокоуровневого языка программирования это неидеально. Однако для численных задач, где эффективность и прозрачность имеют первостепенное значение, альтернативы хуже.
Альтернативой было бы проверка каждого целого числа на переполнение и преобразование результата в целые числа большего размера, такие как Int128 или BigInt в случае переполнения. К сожалению, это вводит существенные накладные расходы при каждой операции с целыми числами (например, при инкрементировании счетчика цикла) — необходимо генерировать код для выполнения проверок переполнения во время выполнения после арифметических инструкций и переходов для обработки потенциальных переполнений. Хуже того, это сделает каждое вычисление, включающее целые числа, type-unstable. Как мы упомянули выше, type-stability имеет решающее значение для эффективной генерации эффективного кода. Если вы не можете рассчитывать на то, что результат операций с целыми числами будет целым числом, невозможно сгенерировать быстрый, простой код так, как это делают компиляторы 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,(Int,))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 1
push RBP
mov RBP, RSP
Source line: 1
lea RAX, QWORD PTR [RDI + 4*RDI - 1]
pop RBP
ret
Фактическое тело функции представляет собой одну инструкцию lea, которая вычисляет целочисленное умножение и сложение сразу. Это ещё более выгодно, когда f подставляется в другую функцию:
julia> function g(k,n)
for i = 1:n
k = f(k)
end
return k
end
g (generic function with 2 methods)
julia> code_native(g,(Int,Int))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 3
push RBP
mov RBP, RSP
test RSI, RSI
jle 22
mov EAX, 1
Source line: 3
lea RDI, QWORD PTR [RDI + 4*RDI - 1]
inc RAX
cmp RAX, RSI
Source line: 2
jle -17
Source line: 5
mov RAX, RDI
pop RBP
ret
Поскольку вызов f подставляется, тело цикла в итоге состоит только из одной инструкции lea. Далее, рассмотрим, что происходит, если мы сделаем количество итераций цикла постоянным:
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,))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 3
push RBP
mov RBP, RSP
Source line: 3
imul RAX, RDI, 9765625
add RAX, -2441406
Source line: 5
pop RBP
ret
Поскольку компилятор знает, что сложение и умножение целых чисел ассоциативны, а умножение распределяется по сложению — ни одно из которых не верно для насыщающей арифметики — он может оптимизировать весь цикл до одного умножения и одного сложения. Насыщающая арифметика полностью блокирует этот вид оптимизации, так как ассоциативность и дистрибутивность могут нарушаться на каждой итерации цикла, вызывая разные результаты в зависимости от того, на какой итерации произойдёт сбой. Компилятор может разворачивать цикл, но не может алгебраически уменьшать несколько операций до меньшего эквивалентного числа операций.
Самый разумный способ избежать ситуации, когда арифметика целых чисел безмолвно переполняется, заключается в том, чтобы везде использовать проверенную арифметику, генерируя ошибки при переполнении при сложении, вычитании и умножении, получая значения, которые не соответствуют значению. В этой статье блога Дан Луу анализирует это и обнаруживает, что, хотя на первый взгляд этот подход должен иметь незначительную стоимость, он на самом деле имеет значительную стоимость из-за того, что компиляторы (LLVM и GCC) не оптимизируют добавление проверок переполнения. Если это улучшится в будущем, мы могли бы рассмотреть возможность по умолчанию использовать проверенную арифметику целых чисел в Julia, но пока мы должны жить с возможностью переполнения.
Пакеты и модули
В чем разница между «using» и «importall»?
Разница всего одна, и на первый взгляд (с точки зрения синтаксиса) она может показаться очень незначительной. Разница между using и importall заключается в том, что с using вам нужно сказать function Foo.bar(.. для расширения функции модуля Foo, bar, новым методом, но с importall или import Foo.bar, вам нужно только сказать function bar(... , и это автоматически расширит функцию bar модуля Foo.
Если вы используете importall, то function Foo.bar(... и function bar(... становятся эквивалентными. Если вы используете using, то они отличаются.
Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширять функцию, о существовании которой вы не знали, потому что это легко может привести к ошибке. Это наиболее вероятно произойдёт с методом, принимающим распространённый тип, такой как строка или целое число, потому что и вы, и другой модуль могли бы определить метод для обработки такого распространённого типа. Если вы используете importall, то вы замените реализацию bar(s::AbstractString) другого модуля своей новой реализацией, которая легко может сделать что-то совершенно другое (и сломать все/многие будущие использования других функций в модуле Foo, которые зависят от вызова bar).
Ничто и отсутствующие значения
Как работают «null» или «ничто» в Julia?
В отличие от многих языков (например, C и Java), Julia не имеет значения «null». При попытке получить доступ к неинициализированной ссылке (переменной, полю объекта или элементу массива) будет немедленно сгенерирована ошибка. Такую ситуацию можно обнаружить с помощью функции isdefined.
Некоторые функции используются только для побочных эффектов и не нуждаются в возвращении значения. В этих случаях принято возвращать значение nothing, которое представляет собой просто одиночный объект типа Void . Это обычный тип без полей; в нём нет ничего особенного, кроме этой условности и того, что REPL не выводит для него ничего. Некоторые языковые конструкции, которые иначе не имели бы значения, также возвращают nothing, например if false; end.
Для ситуаций, когда значение существует не всегда (например, отсутствующие статистические данные), лучше использовать тип Nullable{T}, который позволяет указать тип отсутствующего значения.
Пустой кортеж (()) — ещё одна форма ничто. Но его не следует рассматривать как ничто, а скорее как кортеж из нулевых значений.
В коде, написанном для Julia до версии 0.4, вы иногда можете встретить None, что совсем другое. Это пустой (или «нижний») тип, тип без значений и без подтипов (кроме самого себя). Теперь это записывается как Union{} (пустой тип объединения). Вам обычно не нужно использовать этот тип.
Память
Почему x += y выделяет память, когда x и y являются массивами?
В Julia x += y заменяется во время разбора на x = x + y. Для массивов это означает, что вместо хранения результата в том же месте памяти, что и x, выделяется новый массив для хранения результата.
Хотя такое поведение может удивить некоторых, этот выбор сделан намеренно. Основная причина — наличие объектов immutable в 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 потоковой ввода-вывода синхронный, его реализация полностью асинхронна.
Следующее:
@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 в приведенном выше примере приводит к:
1 Foo Bar 2 Foo Bar 3 Foo Bar
Вы можете заблокировать свои записи с помощью ReentrantLock следующим образом:
l = ReentrantLock()
@sync for i in 1:3
@async begin
lock(l)
try
write(STDOUT, string(i), " Foo ", " Bar ")
finally
unlock(l)
end
end
end
Релизы Julia
Хотите ли вы использовать релизную, бета-версию или ночную версию Julia?
Вы можете предпочесть релизную версию Julia, если ищете стабильную базу кода. Релизы обычно происходят каждые 6 месяцев, предоставляя вам стабильную платформу для написания кода.
Вы можете предпочесть бета-версию Julia, если не против отставания от последних исправлений ошибок и изменений, но предпочитаете немного более быстрый темп изменений. Кроме того, эти бинарные файлы тестируются перед публикацией, чтобы гарантировать их полную функциональность.
Вы можете предпочесть ночную версию Julia, если хотите воспользоваться последними обновлениями языка и не возражаете, если версия, доступная сегодня, иногда не работает.
Наконец, вы также можете рассмотреть возможность сборки Julia из исходного кода. Этот вариант предназначен в основном для тех, кто свободно работает с командной строкой или заинтересован в обучении. Если это про вас, вам также может быть интересно ознакомиться с нашими руководящими принципами для внесения изменений.
Ссылки на каждый из этих типов загрузок можно найти на странице загрузок по адресу http://julialang.org/downloads/. Обратите внимание, что не все версии Julia доступны для всех платформ.
Когда удаляются устаревшие функции?
Устаревшие функции удаляются после последующего релиза. Например, функции, помеченные как устаревшие в релизе 0.1, больше не будут доступны, начиная с релиза 0.2.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/faq/