Spec-Zone.ru › Julia 1.8

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

Общие

Язык 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?

Единственными интерфейсами, стабильными по отношению к SemVer julia версии, являются интерфейсы библиотек Julia Base и стандартных библиотек, описанные в документации и не помеченные как нестабильные (например, экспериментальные и внутренние). Функции, типы и константы не являются частью публичного API, если они не включены в документацию, даже если они имеют docstrings.

Существует полезная недокументированная функция/тип/константа. Могу ли я ее использовать?

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

Документация недостаточно точна. Могу ли я полагаться на существующее поведение?

Пожалуйста, откройте запрос или заявку на добавление для начала обсуждения о превращении существующего поведения в публичный 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.

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

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

#!/bin/bash
#=
exec julia --color=yes --startup-file=no "${BASH_SOURCE[0]}" "$@"
=#

@show ARGS  # put any Julia code here

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

Для того, чтобы перехватить CTRL-C в скрипте, можно использовать

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

@show ARGS  # put any Julia code here

вместо этого. Обратите внимание, что с этим подходом PROGRAM_FILE не будет установлен.

Почему run не поддерживает * или конвейеры для управления внешними программами?

Функция Julia run запускает внешние программы непосредственно без вызова оболочки операционной системы (в отличие от функции system("...") в других языках, таких как Python, R или C). Это означает, что run не выполняет расширение подстановочных знаков * («глоббинг»), а также не интерпретирует конвейеры оболочки, такие как | или >.

Однако вы все еще можете выполнять «глоббинг» и конвейеры, используя функции Julia. Например, встроенная функция pipeline позволяет объединять внешние программы и файлы, аналогично конвейерам оболочек, а пакет Glob.jl реализует совместимый с POSIX «глоббинг».

END_OF_DOCUMENT_MARKER ```

Конечно, вы можете запускать программы через оболочку, явно передавая оболочку и строку команды в run, например, run(`sh -c "ls > files.txt"`) для использования Unix оболочки 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 требует более дисциплинированного подхода к глобальным переменным. У вас есть по крайней мере три варианта:

  1. Поместите код в функцию (так что x будет локальной переменной в функции). В целом, хорошая практика разработки программного обеспечения — использовать функции вместо глобальных скриптов (поищите в Интернете «почему глобальные переменные плохие», чтобы увидеть множество объяснений). В Julia глобальные переменные также медленные.
  2. Оборатите код в let блок. (Это делает x локальной переменной внутри let ... end оператора, опять же исключая необходимость в global).
  3. Явно пометьте 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. Если вам нужно импортировать модуль, но использовать его символы только внутри конкретной функции или набора функций, у вас есть два варианта:

  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 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 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.

END_OF_DOCUMENT_MARKER

Обратите внимание, что это не относится к глобальным переменным, созданным в модуле 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}) at none:1

Это происходит потому, что 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 bar новым методом, а с import Foo.bar, достаточно function bar(... и это автоматически расширит функцию bar модуля Foo.

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

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

  2. количестве одновременно работающих потоков.

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

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

Вычислительный кластер

Как управлять кэшами предварительной компиляции в распределённых файловых системах?

При использовании julia в высокопроизводительных вычислительных (ВВК) установках одновременное вызов n julia процессов создаёт не более n временных копий файлов кэша предварительной компиляции. Если это проблема (медленная и/или небольшая распределённая файловая система), вы можете:

  1. Использовать julia со флагом --compiled-modules=no чтобы отключить предварительную компиляцию.
  2. Настроить частный записываемый хранилище, используя pushfirst!(DEPOT_PATH, private_path), где private_path — это путь, уникальный для этого julia процесса. Это также можно сделать, задав переменную среды JULIA_DEPOT_PATH на значение $private_path:$HOME/.julia.
  3. Создать символическую ссылку от ~/.julia/compiled к каталогу в области временных файлов.

Выпуски Julia

Хотите ли вы использовать стабильную, LTS или ночную версию Julia?

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

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

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

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

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

Как перенести список установленных пакетов после обновления версии Julia?

Каждая мажорная версия julia имеет свою собственную стандартную среду. В результате, при установке новой мажорной версии Julia, пакеты, добавленные вами с использованием предыдущей мажорной версии, по умолчанию не будут доступны. Среда для данной версии julia определяется файлами Project.toml и Manifest.toml в папке, соответствующей номеру версии в .julia/environments/, например .julia/environments/v1.3.

Если вы устанавливаете новую мажорную версию Julia, например 1.4, и хотите использовать в её стандартной среде те же пакеты, что и в предыдущей версии (например, 1.3), вы можете скопировать содержимое файла Project.toml из папки 1.3 в 1.4 . Затем в сеансе новой версии Julia введите «режим управления пакетами», набрав клавишу ], и выполните команду instantiate.

Эта операция определит набор допустимых пакетов из скопированного файла, совместимых с целевой версией Julia, и установит или обновит их, если это подходит. Если вы хотите воспроизвести не только набор пакетов, но и версии, которые вы использовали в предыдущей версии Julia, вы также должны скопировать файл Manifest.toml перед запуском команды Pkg instantiate. Однако обратите внимание, что пакеты могут определять ограничения совместимости, которые могут быть затронуты изменением версии Julia, поэтому точный набор версий, который у вас был в 1.3, может не работать для 1.4.

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

Spec-Zone.ru

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