Spec-Zone.ru › Julia 1.3

Часто задаваемые вопросы

Общие

Является ли Julia именем человека или чего-то?

Нет.

Почему вы не компилируете код Matlab/Python/R/… в Julia?

Поскольку многие люди знакомы с синтаксисом других динамических языков, и много кода уже написано на этих языках, естественно задаться вопросом, почему мы просто не подключили фронтенд Matlab или Python к бэкэнду Julia (или «транспонировали» код в Julia), чтобы получить все преимущества производительности Julia без необходимости программистам изучать новый язык. Просто, правда?

Основная проблема в том, что в компиляторе Julia нет ничего особенного: мы используем обычный компилятор (LLVM) без «секретного ингредиента», о котором другие разработчики языков не знают. Действительно, компилятор Julia во многих отношениях намного проще, чем компиляторы других динамических языков (например, PyPy или LuaJIT). Преимущество производительности Julia почти полностью определяется его фронтендом: семантика его языка позволяет хорошо написанной программе на Julia предоставить компилятору больше возможностей для генерации эффективного кода и структуры памяти. Если вы попытаетесь скомпилировать код Matlab или Python в Julia, наш компилятор будет ограничен семантикой Matlab или Python, что позволит сгенерировать код не лучше, чем у существующих компиляторов для этих языков (и, вероятно, хуже). Ключевая роль семантики также объясняет, почему несколько существующих компиляторов Python (такие как Numba и Pythran) пытаются оптимизировать только небольшой подмножество языка (например, операции с массивами Numpy и скалярами), и для этого подмножества они уже выполняют не хуже, чем мы могли бы с теми же семантиками. Люди, работающие над этими проектами, невероятно умны и достигли удивительных результатов, но адаптация компилятора к языку, который был разработан для интерпретации, является очень сложной задачей.

Преимущество Julia заключается в том, что высокая производительность не ограничивается небольшим подмножеством «встроенных» типов и операций, и можно писать высокоуровневый код с типом-обобщением, который работает с произвольными пользовательскими типами, оставаясь быстрым и эффективным с точки зрения памяти. Типы в таких языках, как Python, просто не предоставляют компилятору достаточной информации для аналогичных возможностей, поэтому как только вы будете использовать эти языки как фронтенд Julia, вы застрянете.

По аналогичным причинам автоматическое преобразование в Julia обычно генерирует нечитабельный, медленный, неидиоматический код, который не будет хорошей отправной точкой для портирования нативный Julia из другого языка.

С другой стороны, взаимодействие языков чрезвычайно полезно: мы хотим использовать существующий высококачественный код на других языках из Julia (и наоборот)! Лучший способ сделать это — не транспилятор, а удобные средства межъязыкового вызова. Мы много работали над этим, от встроенного ccall встроенного (для вызова библиотек C и Fortran) до пакетов JuliaInterop, которые соединяют Julia с Python, Matlab, C++ и другими.

Сеансы и REPL

Как удалить объект в памяти?

Julia не имеет аналога функции MATLAB clear; после того, как имя определено в сеансе Julia (технически, в модуле Main), оно всегда присутствует.

Если вас беспокоит использование памяти, вы всегда можете заменить объекты на объекты, потребляющие меньше памяти. Например, если A — это массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = nothing. Память будет освобождена при следующем запуске сборщика мусора; вы можете заставить это произойти с помощью gc(). Кроме того, попытка использовать A, вероятно, приведет к ошибке, поскольку большинство методов не определены для типа Nothing.

Как я могу изменить объявление типа в моем сеансе?

Возможно, вы определили тип, а затем поняли, что вам нужно добавить новое поле. Если вы попробуете это в REPL, вы получите ошибку:

ERROR: invalid redefinition of constant MyType

Типы в модуле Main не могут быть переопределены.

Хотя это может быть неудобно при разработке нового кода, есть отличное решение. Модули могут быть заменены переопределением, и поэтому, если вы заключите весь свой новый код в модуль, вы можете переопределить типы и константы. Вы не можете импортировать имена типов в Main и ожидать, что сможете переопределить их там, но вы можете использовать имя модуля для разрешения области видимости. Другими словами, при разработке вы можете использовать рабочий процесс, похожий на этот:

include("mynewcode.jl")              # this defines a module MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Got an error. Change something in "mynewcode.jl"
include("mynewcode.jl")              # reload the module
obj1 = MyModule.ObjConstructor(a, b) # old objects are no longer valid, must reconstruct
obj2 = MyModule.somefunction(obj1)   # this time it worked!
obj3 = MyModule.someotherfunction(obj2, c)
...

Сценарии

Как проверить, запускается ли текущий файл как главный скрипт?

При запуске файла как главного скрипта с помощью julia file.jl может потребоваться включить дополнительную функциональность, такую как обработка аргументов командной строки. Способ определения того, что файл запущен таким образом, — это проверка, является ли abspath(PROGRAM_FILE) == @__FILE__ true.

Как перехватить CTRL-C в скрипте?

Запуск скрипта Julia с помощью julia file.jl не вызывает InterruptException при попытке завершить его с помощью CTRL-C (SIGINT). Чтобы запустить определенный код перед завершением скрипта Julia, который может или не может быть вызван CTRL-C, используйте atexit. В качестве альтернативы, вы можете использовать julia -e 'include(popfirst!(ARGS))' file.jl для выполнения скрипта и перехвата InterruptException в блоке try.

Как передать параметры julia с помощью #!/usr/bin/env?

Передача параметров julia в так называемом shebang, например, #!/usr/bin/env julia --startup-file=no, может не работать на некоторых платформах, таких как Linux. Это происходит потому, что обработка аргументов в shebang зависит от платформы и не определена однозначно. В среде типа Unix надежным способом передачи параметров julia в исполняемом скрипте будет запуск скрипта как скрипта bash и использование exec для замены процесса на julia:

#!/bin/bash
#=
exec julia --color=yes --startup-file=no -e 'include(popfirst!(ARGS))' \
    "${BASH_SOURCE[0]}" "$@"
=#

@show ARGS  # put any Julia code here

В приведенном выше примере код между #= и =# выполняется как скрипт bash. Julia игнорирует эту часть, поскольку это многострочный комментарий для Julia. Код Julia после =# игнорируется bash, так как он останавливает обработку файла, достигнув инструкции exec.

Функции

Я передал аргумент 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. Если вам нужно импортировать модуль, но использовать его символы только внутри определенной функции или набора функций, у вас есть два варианта:

  1. Используйте import:

    import Foo
    function bar(...)
        # ... refer to Foo symbols via Foo.baz ...
    end

    Это загружает модуль Foo и определяет переменную Foo, которая ссылается на модуль, но не импортирует какие-либо другие символы из модуля в текущее пространство имен. Вы обращаетесь к символам Foo по их квалифицированным именам Foo.bar и т. д.

  2. Заключение вашей функции в модуль:

    module Bar
    export bar
    using Foo
    function bar(...)
        # ... refer to Foo.baz as simply baz ....
    end
    end
    using Bar

    Это импортирует все символы из Foo, но только внутри модуля Bar.

Что делает оператор ...?

Два использования оператора ...: «сливание» и «раскладывание»

Многие новички в Julia находят использование оператора ... непонятным. Часть того, что делает оператор ... непонятным, заключается в том, что он означает разные вещи в зависимости от контекста.

... объединяет множество аргументов в один аргумент в определениях функций

В контексте определений функций оператор ... используется для объединения многих аргументов в один аргумент. Это использование оператора ... для объединения многих аргументов в один аргумент называется «сливанием»:

julia> function printargs(args...)
           println(typeof(args))
           for (i, arg) in enumerate(args)
               println("Arg #$i = $arg")
           end
       end
printargs (generic function with 1 method)

julia> printargs(1, 2, 3)
Tuple{Int64,Int64,Int64}
Arg #1 = 1
Arg #2 = 2
Arg #3 = 3

Если бы Julia использовала более свободно символы ASCII, оператор «сливания» можно было бы записать как <-... вместо ....

... разделяет один аргумент на множество разных аргументов в вызовах функций

В отличие от использования оператора ... для обозначения «сливания» многих аргументов в один аргумент при определении функции, оператор ... также используется для разделения одного аргумента на множество аргументов при использовании в контексте вызова функции. Это использование оператора ... называется «раскладыванием»:

julia> function threeargs(a, b, c)
           println("a = $a::$(typeof(a))")
           println("b = $b::$(typeof(b))")
           println("c = $c::$(typeof(c))")
       end
threeargs (generic function with 1 method)

julia> x = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> threeargs(x...)
a = 1::Int64
b = 2::Int64
c = 3::Int64

Если бы Julia была языком, который более активно использовал символы ASCII, оператор распаковки, возможно, был бы написан как ...-> вместо ....

Что возвращает оператор присваивания?

Оператор = всегда возвращает правую часть, поэтому:

julia> function threeint()
           x::Int = 3.0
           x # returns variable x
       end
threeint (generic function with 1 method)

julia> function threefloat()
           x::Int = 3.0 # returns 3.0
       end
threefloat (generic function with 1 method)

julia> threeint()
3

julia> threefloat()
3.0

и аналогично:

julia> function threetup()
           x, y = [3, 3]
           x, y # returns a tuple
       end
threetup (generic function with 1 method)

julia> function threearr()
           x, y = [3, 3] # returns an array
       end
threearr (generic function with 1 method)

julia> threetup()
(3, 3)

julia> threearr()
2-element Array{Int64,1}:
 3
 3

Типы, объявления типов и конструкторы

Что означает «стабильность типа»?

Это означает, что тип результата предсказуем на основе типов входных данных. В частности, это означает, что тип результата не может меняться в зависимости от значений входных данных. Следующий код не является типостабильным:

julia> function unstable(flag::Bool)
           if flag
               return 1
           else
               return 1.0
           end
       end
unstable (generic function with 1 method)

Он возвращает либо Int, либо Float64 в зависимости от значения его аргумента. Поскольку Julia не может предсказать тип результата во время компиляции, любая вычисление, использующая эту функцию, должна учитывать возможность обоих типов, что затрудняет генерацию быстрого машинного кода.

Почему Julia возвращает DomainError для определенных, на первый взгляд, осмысленных операций?

Определенные операции математически осмысленны, но приводят к ошибкам:

julia> sqrt(-2.0)
ERROR: DomainError with -2.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Это поведение является неудобным следствием требования типостабильности. В случае с sqrt, большинство пользователей ожидают, что sqrt(2.0) вернёт вещественное число, и будут недовольны, если он вернет комплексное число 1.4142135623730951 + 0.0im. Можно было бы написать функцию sqrt, чтобы переключиться на комплексное значение результата только при передаче отрицательного числа (что делает sqrt в некоторых других языках), но тогда результат не был бы типостабильным, а функция sqrt имела бы низкую производительность.

В этих и других случаях можно получить желаемый результат, выбрав тип входных данных, который указывает готовность принять тип выходных данных, в котором результат может быть представлен:

julia> sqrt(-2.0+0im)
0.0 + 1.4142135623730951im

Почему Julia использует машинную арифметику для целых чисел?

Julia использует машинную арифметику для вычислений с целыми числами. Это означает, что диапазон Int значений ограничен и переполняется по обеим сторонам, так что сложение, вычитание и умножение целых чисел могут переполняться, что приводит к некоторым результатам, которые могут быть неудивительными на первый взгляд:

julia> typemax(Int)
9223372036854775807

julia> ans+1
-9223372036854775808

julia> -ans
-9223372036854775808

julia> 2*ans
0

Очевидно, это сильно отличается от того, как ведут себя математические целые числа, и можно подумать, что для высокоуровневого языка программирования не очень хорошо это демонстрировать пользователю. Однако для численных вычислений, где эффективность и прозрачность имеют первостепенное значение, альтернативы хуже.

Одним из вариантов было бы проверять каждое целое числовое действие на переполнение и продвигать результаты к более крупным типам целых чисел, таким как Int128 или BigInt в случае переполнения. К сожалению, это вносит значительную нагрузку на каждую операцию с целыми числами (например, при инкрементировании счётчика цикла) – требуется генерировать код для проверки переполнения во время выполнения после арифметических инструкций и переходов для обработки потенциальных переполнений. Ещё хуже, это сделало бы все вычисления с целыми числами нетипостабильными. Как мы упоминали выше, типостабильность имеет важное значение для эффективной генерации эффективного кода. Если нельзя положиться на то, что результаты целых чисел будут целыми числами, невозможно сгенерировать быстрый, простой код так, как это делают компиляторы C и Fortran.

Вариант этого подхода, который избегает внешнего вида нестабильности типа, состоит в том, чтобы объединить типы Int и BigInt в один гибридный тип целых чисел, который внутренне меняет представление, когда результат больше не помещается в размер машинного целого числа. Хотя это внешне предотвращает нестабильность типа на уровне кода Julia, это просто переносит проблему на C-код, реализующий этот гибридный тип целых чисел. Этот подход может сработать и даже быть достаточно быстрым во многих случаях, но имеет несколько недостатков. Одна проблема заключается в том, что представление целых чисел в памяти и массивов целых чисел больше не соответствует естественному представлению, используемому C, Fortran и другими языками с машинными целыми числами. Таким образом, для взаимодействия с этими языками нам в конечном итоге придётся ввести и родные типы целых чисел. Любое неограниченное представление целых чисел не может иметь фиксированное число битов и, следовательно, не может храниться в массиве с фиксированными слотами – большие значения целых чисел всегда требуют отдельного выделения памяти на куче. И, конечно же, независимо от того, насколько умной является реализация гибридных целых чисел, всегда есть подводные камни производительности – ситуации, когда производительность неожиданно ухудшается. Сложное представление, отсутствие взаимодействия с C и Fortran, невозможность представления массивов целых чисел без дополнительного выделения памяти на куче и непредсказуемые характеристики производительности делают даже самые умные реализации гибридных целых чисел плохим выбором для высокопроизводительных численных вычислений.

Альтернативой использованию гибридных целых чисел или продвижению к BigInts является использование насыщенной арифметики целых чисел, где добавление к наибольшему значению целого числа оставляет его неизменным, а также для вычитания из наименьшего значения целого числа. Именно это делает Matlab™:

>> int64(9223372036854775807)

ans =

  9223372036854775807

>> int64(9223372036854775807) + 1

ans =

  9223372036854775807

>> int64(-9223372036854775808)

ans =

 -9223372036854775808

>> int64(-9223372036854775808) - 1

ans =

 -9223372036854775808

На первый взгляд, это достаточно разумно, так как 9223372036854775807 ближе к 9223372036854775808, чем -9223372036854775808, и целые числа по-прежнему представляются с фиксированной длиной естественным образом, совместимым с C и Fortran. Однако насыщенная арифметика целых чисел сильно проблематична. Первая и наиболее очевидная проблема заключается в том, что это не то, как работает машинная арифметика целых чисел, поэтому реализация насыщенных операций требует выдачи инструкций после каждой операции с машинными целыми числами для проверки переполнения или недополнения и замены результата на typemin(Int) или typemax(Int) соответственно. Это само по себе расширяет каждую операцию с целыми числами с одной быстрой инструкции до полудюжины инструкций, вероятно, включая переходы. Ого. Но становится хуже – насыщенная арифметика целых чисел не ассоциативна. Рассмотрим это вычисление Matlab:

>> n = int64(2)^62
4611686018427387904

>> n + (n - 1)
9223372036854775807

>> (n + n) - 1
9223372036854775806

Это затрудняет написание многих основных алгоритмов с целыми числами, поскольку многие общие методы зависят от того, что машинная операция сложения с переполнением является ассоциативной. Рассмотрим поиск середины между целыми значениями lo и hi в Julia с помощью выражения (lo + hi) >>> 1:

julia> n = 2^62
4611686018427387904

julia> (n + 2n) >>> 1
6917529027641081856

Видите? Никаких проблем. Это правильная середина между 2^62 и 2^63, несмотря на то, что n + 2n равно -4611686018427387904. Теперь попробуйте это в Matlab:

>> (n + 2*n)/2

ans =

  4611686018427387904

Упс. Добавление оператора >>> к Matlab не поможет, потому что насыщение, которое происходит при добавлении n и 2n, уже разрушило информацию, необходимую для вычисления правильной середины.

Отсутствие ассоциативности не только неудобно для программистов, которые не могут положиться на него для таких техник, но также препятствует тому, чтобы компиляторы могли оптимизировать арифметику целых чисел. Например, поскольку целые числа Julia используют обычную машинную арифметику целых чисел, LLVM свободно агрессивно оптимизирует простые небольшие функции, такие как f(k) = 5k-1. Машинный код для этой функции выглядит так:

julia> code_native(f, Tuple{Int})
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 1
  leaq  -1(%rdi,%rdi,4), %rax
  popq  %rbp
  retq
  nopl  (%rax,%rax)

Фактическое тело функции – это одна инструкция leaq, которая вычисляет умножение и сложение целых чисел одновременно. Это ещё более выгодно, когда f инлайнится в другую функцию:

julia> function g(k, n)
           for i = 1:n
               k = f(k)
           end
           return k
       end
g (generic function with 1 methods)

julia> code_native(g, Tuple{Int,Int})
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 2
  testq %rsi, %rsi
  jle L26
  nopl  (%rax)
Source line: 3
L16:
  leaq  -1(%rdi,%rdi,4), %rdi
Source line: 2
  decq  %rsi
  jne L16
Source line: 5
L26:
  movq  %rdi, %rax
  popq  %rbp
  retq
  nop

Поскольку вызов f инлайнится, тело цикла оказывается просто одной инструкцией leaq. Далее, рассмотрим, что произойдёт, если мы сделаем число итераций цикла фиксированным:

julia> function g(k)
           for i = 1:10
               k = f(k)
           end
           return k
       end
g (generic function with 2 methods)

julia> code_native(g,(Int,))
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 3
  imulq $9765625, %rdi, %rax    # imm = 0x9502F9
  addq  $-2441406, %rax         # imm = 0xFFDABF42
Source line: 5
  popq  %rbp
  retq
  nopw  %cs:(%rax,%rax)

Поскольку компилятор знает, что сложение и умножение целых чисел ассоциативны, а умножение распределяется по сложению – ни то, ни другое не верно для насыщенной арифметики – он может оптимизировать весь цикл до одного умножения и одного сложения. Насыщенная арифметика полностью подавляет такой вид оптимизации, так как ассоциативность и дистрибутивность могут нарушаться на каждой итерации цикла, вызывая разные результаты в зависимости от того, на какой итерации произошло нарушение. Компилятор может развернуть цикл, но не может алгебраически сократить несколько операций до меньшего количества эквивалентных операций.

Самая разумная альтернатива тому, чтобы арифметика целых чисел молча переполнялась, – это выполнить проверку арифметики во всех случаях, вызывая ошибки, когда сложения, вычитания и умножения переполняются, получая значения, не соответствующие значениям.

В этой статье блога Дан Луу анализирует это и обнаруживает, что вместо тривиальных затрат, которые этот подход должен иметь теоретически, он оказывается дорогостоящим из-за того, что компиляторы (LLVM и GCC) не оптимизируют добавление проверок переполнения.

Какие возможные причины ошибки UndefVarError во время удалённого выполнения?

Как указано в сообщении об ошибке, непосредственной причиной ошибки UndefVarError на удалённом узле является то, что связывание с этим именем не существует. Давайте рассмотрим некоторые возможные причины.

julia> module Foo
           foo() = remotecall_fetch(x->x, 2, "Hello")
       end

julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: Foo not defined
Stacktrace:
[...]

Замыкание x->x содержит ссылку на Foo, и поскольку Foo недоступно на узле 2, выбрасывается ошибка UndefVarError.

Глобальные переменные в модулях, отличных от Main, не сериализуются по значению на удалённый узел. Отправляется только ссылка. Функции, которые создают глобальные привязки (кроме тех, что находятся в Main ), могут привести к тому, что ошибка UndefVarError будет выброшена позже.

julia> @everywhere module Foo
           function foo()
               global gvar = "Hello"
               remotecall_fetch(()->gvar, 2)
           end
       end

julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: gvar not defined
Stacktrace:
[...]

В приведенном выше примере @everywhere module Foo определял Foo на всех узлах. Однако вызов Foo.foo() создал новую глобальную привязку gvar на локальном узле, но её не было на узле 2, что привело к ошибке UndefVarError.

Обратите внимание, что это не относится к глобальным переменным, созданным в модуле Main. Глобальные переменные в модуле Main сериализуются, а новые привязки, созданные в Main , создаются на удалённом узле.

julia> gvar_self = "Node1"
"Node1"

julia> remotecall_fetch(()->gvar_self, 2)
"Node1"

julia> remotecall_fetch(varinfo, 2)
name          size summary
––––––––– –––––––– –––––––
Base               Module
Core               Module
Main               Module
gvar_self 13 bytes String

Это не относится к объявлениям function или struct. Однако анонимные функции, привязанные к глобальным переменным, сериализуются, как показано ниже.

julia> bar() = 1
bar (generic function with 1 method)

julia> remotecall_fetch(bar, 2)
ERROR: On worker 2:
UndefVarError: #bar not defined
[...]

julia> anon_bar  = ()->1
(::#21) (generic function with 1 method)

julia> remotecall_fetch(anon_bar, 2)
1

Почему Julia использует * для конкатенации строк? Почему не + или что-то другое?

Основным аргументом против * является то, что конкатенация строк не коммутативна, в то время как + обычно используется как коммутативный оператор. Хотя сообщество Julia признаёт, что другие языки используют разные операторы, и * может быть непривычным для некоторых пользователей, он отражает определённые алгебраические свойства.

Обратите внимание, что вы также можете использовать  + для конкатенации строк (и других значений, преобразованных в строки); аналогично,  * может быть использован вместо  * для повторения строк. Синтаксис интерполяции также полезен для построения строк.

Пакеты и модули

В чём разница между «using» и «import»?

Существует только одно различие, и на первый взгляд (с точки зрения синтаксиса) оно может показаться очень незначительным. Разница между using и import заключается в том, что при использовании using вам нужно указать :: для расширения функции модуля Foo с новой функцией, но при использовании import, вам достаточно указать функцию и она автоматически расширит функцию модуля Foo.

Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширить функцию, о существовании которой вы не знали, потому что это может легко привести к ошибке. Это наиболее вероятно произойдёт с методом, который принимает общий тип, такой как строка или целое число, потому что как вы, так и другой модуль можете определить метод для обработки такого общего типа. Если вы используете using, вы замените реализацию другого модуля с вашей новой реализацией, что может легко сделать что-то совершенно другое (и сломать все/многие последующие использования других функций в модуле 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.

Пустой кортеж (()) — ещё одна форма ничто. Но его следует рассматривать не как ничто, а как кортеж из нулевых значений.

Пустой (или «нижний») тип, записанный как Union{} (пустой тип объединения), — это тип без значений и без подтипов (кроме себя). Вам, как правило, не нужно использовать этот тип.

Память

Почему x ⋅ y выделяет память, когда x и y являются массивами?

В Julia x += y заменяется во время парсинга на x = x + y. Для массивов это означает, что вместо хранения результата в том же месте памяти, что и x, он выделяет новый массив для хранения результата.

Хотя это поведение может удивить некоторых, выбор сделан осознанно. Основная причина — наличие неизменяемых объектов в Julia, которые не могут изменить своё значение после создания. Действительно, число — неизменяемый объект; операторы x = 5; x += 1 не изменяют значение 5, они изменяют значение, привязанное к x. В случае неизменяемого объекта единственный способ изменить значение — переназначить его.

Для более глубокого понимания рассмотрим следующую функцию:

function power_by_squaring(x, n::Int)
    ispow2(n) || error("This implementation only works for powers of 2")
    while n >= 2
        x *= x
        n >>= 1
    end
    x
end

После вызова, например, x = 5; y = power_by_squaring(x, 4), вы получите ожидаемый результат: x == 5 && y == 625. Однако теперь предположим, что *=, при использовании с матрицами, вместо этого изменял бы левую часть. Возникло бы две проблемы:

  • В случае общих квадратных матриц A = A*B невозможно реализовать без временного хранения: A[1,1] вычисляется и сохраняется в левой части перед тем, как вы закончите его использовать в правой части.
  • Предположим, что вы были готовы выделить временное хранилище для вычисления (что лишало бы большую часть смысла в том, чтобы делать *= ин-плейс); если бы вы воспользовались изменяемостью x, эта функция по-разному бы вела себя для изменяемых и неизменяемых входных данных. В частности, для неизменяемого x, после вызова у вас (в общем случае) было бы y != x, но для изменяемого x у вас было бы y == x.

Поскольку поддержка универсального программирования считается более важной, чем потенциальные оптимизации производительности, которые могут быть достигнуты другими способами (например, с помощью явных циклов), операторы, такие как += и *=, работают путём перепривязки новых значений.

Асинхронный ввод-вывод и одновременные синхронные записи

Почему одновременные записи в один и тот же поток приводят к перемешанному выводу?

Хотя API потокового ввода-вывода является синхронным, его реализация полностью асинхронна.

Рассмотрим вывод, напечатанный из следующего:

julia> @sync for i in 1:3
           @async write(stdout, string(i), " Foo ", " Bar ")
       end
123 Foo  Foo  Foo  Bar  Bar  Bar

Это происходит потому, что, хотя вызов write является синхронным, запись каждого аргумента уступает другим задачам, ожидая завершения этой части ввода-вывода.

print и println «блокируют» поток во время вызова. Поэтому изменение write на println в приведённом выше примере приводит к:

julia> @sync for i in 1:3
           @async println(stdout, string(i), " Foo ", " Bar ")
       end
1 Foo  Bar
2 Foo  Bar
3 Foo  Bar

Вы можете заблокировать свои записи с помощью ReentrantLock следующим образом:

julia> l = ReentrantLock();

julia> @sync for i in 1:3
           @async begin
               lock(l)
               try
                   write(stdout, string(i), " Foo ", " Bar ")
               finally
                   unlock(l)
               end
           end
       end
1 Foo  Bar 2 Foo  Bar 3 Foo  Bar

Массивы

В чём разница между нульмерными массивами и скалярами?

Нульмерные массивы имеют вид Array{T,0}. Они ведут себя как скаляры, но есть важные различия. Они заслуживают особого упоминания, потому что представляют собой особый случай, который логично вытекает из общего определения массивов, но может быть несколько неинтуитивным на первый взгляд. Следующая строка определяет нульмерный массив:

julia> A = zeros()
0-dimensional Array{Float64,0}:
0.0

В этом примере A — это изменяемый контейнер, который содержит один элемент, который можно установить с помощью A[] = 1.0 и получить с помощью A[]. Все нульмерные массивы имеют одинаковый размер (size(A) == ()) и длину (length(A) == 1). В частности, нульмерные массивы не пусты. Если вам это кажется неинтуитивным, вот несколько идей, которые помогут понять определение массивов в Julia.

  • Нульмерные массивы — это «точка» по отношению к «линии» векторов и «плоскости» матриц. Так же как линия не имеет площади (но всё ещё представляет набор вещей), точка не имеет длины или каких-либо измерений (но всё ещё представляет нечто).
  • Мы определяем prod(()) как 1, а общее количество элементов в массиве — произведение размеров. Размер нульмерного массива — (), а его длина — 1.
  • Нульмерные массивы изначально не имеют измерений, в которые вы индексируете — они просто A[]. Мы можем применить то же правило «последнего единицы» для них, что и для всех других размерностей массивов, так что вы можете индексировать их как A[1], 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, причина расхождения, скорее всего, заключается в

  1. библиотека BLAS, используемая каждым языком,

  2. количество потоков.

Julia компилирует и использует свою собственную копию OpenBLAS, при этом количество потоков ограничено значением 8 (или количеством ваших ядер).

Изменение настроек OpenBLAS или компиляция Julia с другой библиотекой BLAS, например, Intel MKL, может улучшить производительность. Вы можете использовать MKL.jl, пакет, который заставляет Julia использовать BLAS и LAPACK Intel MKL вместо OpenBLAS, или искать предложения в форуме обсуждений о том, как это настроить вручную. Обратите внимание, что Intel MKL не может быть включён в Julia, так как он не является открытым исходным кодом.

Julia Releases

Do I want to use the Stable, LTS, or nightly version of Julia?

Версия Julia Stable — это последняя выпущенная версия Julia, которую большинство пользователей захотят запустить. Она содержит новейшие функции, включая улучшенную производительность. Версия Julia Stable имеет версию v1.x.y, соответствующую SemVer. Новый выпуск Julia, соответствующий новой Stable версии, выпускается примерно каждые 4-5 месяцев после нескольких недель тестирования в качестве кандидата на выпуск. В отличие от LTS-версии, стабильная версия Julia обычно не получает исправления ошибок после выпуска другой стабильной версии Julia. Однако обновление до следующего стабильного выпуска всегда возможно, так как каждый выпуск Julia v1.x будет продолжать работать с кодом, написанным для более ранних версий.

Вы можете предпочесть LTS (Long Term Support) версию Julia, если вам нужна очень стабильная среда кода. Текущая LTS-версия Julia имеет версию v1.0.x, соответствующую SemVer; этот раздел будет продолжать получать исправления ошибок до тех пор, пока не будет выбран новый LTS-раздел. В этот момент серия v1.0.x больше не будет регулярно получать исправления ошибок, и всем, кроме наиболее консервативных пользователей, будет рекомендовано перейти на новую серию LTS-версий. Как разработчику пакетов, вы можете предпочесть разработку для LTS-версии, чтобы максимизировать количество пользователей, которые могут использовать ваш пакет. Согласно SemVer, код, написанный для v1.0, будет продолжать работать для всех будущих LTS и стабильных версий. В общем случае, даже если вы ориентируетесь на LTS, вы можете разрабатывать и запускать код в последней стабильной версии, чтобы воспользоваться улучшенной производительностью; при условии, что вы избегаете использования новых функций (таких как добавленные функции библиотеки или новые методы).

Вы можете предпочесть ночную версию Julia, если хотите воспользоваться последними обновлениями языка и не возражаете, если версия, доступная сегодня, иногда не работает. Как следует из названия, релизы ночной версии производятся примерно каждую ночь (в зависимости от стабильности инфраструктуры сборки). В целом, ночные версии довольно безопасны в использовании — ваш код не загорится. Однако могут быть случайные регрессии или проблемы, которые не будут обнаружены до более тщательного предварительного тестирования. Вы можете протестировать ночную версию, чтобы убедиться, что такие регрессии, влияющие на ваш случай использования, будут обнаружены до выпуска.

Наконец, вы можете рассмотреть возможность построения Julia из исходного кода самостоятельно. Этот вариант предназначен в основном для тех, кто свободно ориентируется в командной строке или интересуется изучением. Если это про вас, вам также может быть интересно ознакомиться с нашими руководствами по участию.

Ссылки на каждый из этих типов загрузок можно найти на странице загрузки по адресу https://julialang.org/downloads/. Обратите внимание, что не все версии Julia доступны для всех платформ.

© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.3.1/manual/faq/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API