1.8. Массивы и broadcast#
В Julia для массивов существует абстрактный тип AbstractArray{T,N}, где T тип элемента массива (Int64, String, Complex{Int64}, Number, Any…), а N размерность. При написании собственных типов с поведением массива в объявлении следует указать, что ваш тип является подтипом AbstractArray.
Встроенным типом для массивом является Array{T,N}. Это изменяемый массив динамического размера с column-major хранением элементов.
Примечание
Поскольку в Julia массивы хранятся постолбично, то для быстродействия вложенный цикл должен проходится по строке, а внешний по столбцу. Вообще, если есть массив A[i, j, k], то цикл должен выглядить так:
for k in eachindex(A, 3)
for j in eachindex(A, 2)
for i in eachindex(A, 1)
...
end
end
end
Существуют два часто используемых синонима
Vector{T} == Array{T,1}для одномерных вектор-столбцов;Matrix{T} == Array{T,2}для матриц и вектор-строк.
Литералом Array{T,N} являются квадратные скобки [], при этом можно явно указать тип T[] (Int[1, 2], Float64[1, 2]).
Если тип элементов массива не указывать, то Julia его вычислит.
Для создания массива через литерал используется вертикальная и горизонтальная конкатенация.
Для вертикальной конкатенации используется ; или новая строка.
julia> [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> [[1, 2, 3]; [7, 8]]
5-element Vector{Int64}:
1
2
3
7
8
Для горизонтальной конкатенации используется пробел или табуляция. Запятая , означает отсутствие конкатенации.
julia> [1 2 3 4]
1×4 Matrix{Int64}:
1 2 3 4
julia> [[1, 2] [3, 4]]
2×2 Matrix{Int64}:
1 3
2 4
Если конкатенация не нужна, используйтся запятая ,. В примере ниже показано создание вектора векторов.
julia> [[1, 2, 3], [7, 8]]
2-element Vector{Vector{Int64}}:
[1, 2, 3]
[7, 8]
На первый взгляд синтаксис может показаться запутанным. Однако, с ним легко создавать блочные матрицы и тензоры.
Скажем, задана блочная матрица
Соответствующий код будет
julia> A = [1 2 3; 4 5 6; 7 8 9] # или reshape(collect(1:9), 3, 3)'
3×3 Matrix{Int64}:
1 2 3
4 5 6
7 8 9
julia> I = [1 0 0; 0 1 0; 0 0 1]
3×3 Matrix{Int64}:
1 0 0
0 1 0
0 0 1
julia> zero = [0, 0, 0] # или zeros(Int, 3)
3-element Vector{Int64}
0
0
0
julia> b = [10, 11, 12] # или collect(10:12)
3-element Vector{Int64}:
10
11
12
julia> M = [A I; zero' b']
4×6 Matrix{Int64}:
1 2 3 1 0 0
4 5 6 0 1 0
7 8 9 0 0 1
0 0 0 10 11 12
Оператор ' (одинарная кавычка) выполняет транспонирование.
1.8.1. Индексирование#
В Julia встроено два вида индексов.
Синтаксис первого A[i], это линейный индекс LinearIndex.
Он позволяет получить i-ый по счёту в памяти элемент массива.
Например, для вектора A[4] это \(A_4\), а для матрицы размера \(2 \times 3\) это \(A_{2, 2}\).
Синтаксис второго A[i, j, k,..], это декартов индекс CartesianIndex. Данный индекс учитывает размерность массива. Например, для матрицы элемент A[i, j] это элемент \(A_{i,j}\) (\(i\)-ая строка, \(j\)-ый столбец).
Индексом может быть и коллекция элементов, что позволяет обращаться к целым строкам, столцам, их пересечению, или просто некоторому набору элементов. Часто для этого используется range operator start:[step:]stop.
Ниже показано несколько примеров для матрицы.
julia> A = [1 2 3; 4 5 6; 7 8 9]
3×3 Matrix{Int64}:
1 2 3
4 5 6
7 8 9
julia> A[6] # линейный индекс
8
julia> A[3, 2] # декартов индекс
8
julia> A[end, end]
9
Строка.
julia> A[1, :] # A[строка 1, все столбцы]
3-element Vector{Int64}:
1
2
3
Столбец.
julia> A[:, 2] # A[все строки, столбец 2]
3-element Vector{Int64}:
2
5
8
«Пересечение» строк 1 и 2 со столбцами 2 и 3.
julia> A[1:2, 2:3]
2×2 Matrix{Int64}:
2 3
5 6
Угольные элементы.
julia> A[[1, end], [1, end]]
2×2 Matrix{Int64}:
1 3
7 9
Главная диагональ.
julia> A[1:4:end]
3-element Vector{Int64}:
1
5
9
Побочная диагональ.
julia> A[3:2:7]
3-element Vector{Int64}:
7
5
3
1.8.2. Присваивание значений#
Массив Array{T,N} изменяемый тип, можно перезаписывать его элементы. Это делается с помощью оператора присваивания =, при этом по обе стороны должны находиться объекты одинаковых размерностей.
julia> A = [1 2 3; 4 5 6; 7 8 9];
julia> A[:, 1] = [7, 4, 1];
julia> A
3×3 Matrix{Int64}:
7 2 3
4 5 6
1 8 9
julia> A[1, :] = [-1, 0, 1];
julia> A
3×3 Matrix{Int64}:
-1 0 1
4 5 6
1 8 9
julia> A[1, 2] = 10;
julia> A
3×3 Matrix{Int64}:
-1 10 1
4 5 6
1 8 9
При присваивании можно использовать векторизацию.
julia> A[1, :] .= 11;
julia> A
3×3 Matrix{Int64}:
11 11 11
4 5 6
1 8 9
1.8.3. Вспомогательные функции#
Интроспекция массива
length(A): количество элементов;ndims(A): размерность;size(A): кортеж из размеров каждой размерности(число_строк, число_столбцов);size(A, n): размер размерностиn;eachindex(A): итератор из индексов массива.
Создание массивов
collect(c): создание массива из коллекции;zeros([T=Float64,] dims...): массив из нулей типаTразмерностиdims...;ones([T=Float64,] dims...): аналогичноzeros, но массив из единиц;copy(A): нерекурсивная копия массива;similar(A): неинициализированный массив того же типа и размера, что иA;empty(A): пустой массив того же вида, что иA;rand([T=Float64,] dims...): массив из равномерно-распределённых случайных чисел;fill(x, dims...): массив размераdims...изx;map(f, c): создание массива на основе применения функцииfк каждому элементу коллекцииc.
Другие операции
push!(A, x...): добавление в конецAэлементаx;pop!(A): извлечение элемента с концаA.
Для чтения и записи в файл массивов в форматах с сепаратором (.csv, .tsv,..) есть встроенная библиотека DelimitedFiles и сторонний пакет CSV.jl.
1.8.4. Векторизация функций#
Совет
Соответствующий раздел мануала [url].
В Julia любая функция может быть векторизована. Т.е. вы можете создать функцию для скаляров и автоматически получаете её векторизованный аналог. Векторизация осуществляется через механизм broadcasting. При этом достаточно в конце имени функции дописать точку . (dot-syntax).
julia> x = [0, π/6, π/3];
julia> sin(x)
ERROR: MethodError: no method matching sin(::Vector{Float64})
...
julia> sin.(x)
3-element Vector{Float64}:
0.0
0.49999999999999994
0.8660254037844386
Механизм векторизации «насыщен» (fused). Для программиста это означает, что сложный вызов, например, sin.(cos.(x)) не медленнее цикла, в котором к каждому элементу вектора x применили sin(cos(x[i])).
Автоматически векторизуются и пользовательские функции над стандартными типами.
julia> f(x) = x^2 + 1
f (generic function with 1 method)
julia> f.([1, 2, 3])
3-element Vector{Int64}:
2
5
10
julia> g(x, y) = f(x) + f(y)
g (generic function with 1 method)
julia> g.([1, 2], [3, 4])
2-element Vector{Int64}:
12
22
julia> g.([1, 2], 3)
2-element Vector{Int64}:
12
15
julia> parse.(Float64, ["1", "2.5", "-1.2e-9"])
3-element Vector{Float64}:
1.0
2.5
-1.2e-9
Для собственных типов нужно объявлять правила векторизации. Однако, если ваш тип должен вести себя как скаляр, можно воспользоваться Ref(x).
Ниже определяется модель идеального газа и его давление. Затем, с помощью Ref и векторизации вычисляется давление кислорода при фиксированной плотности для ряда температур.
julia> struct IdealGasComponent
name::String
molarmass::Float64
end
julia> pressure(c::IdealGasComponent, ρ, T) = ρ * 8.314 * T / c.molarmass
pressure (generic function with 1 method)
julia> oxygen = IdealGasComponent("oxygen", 0.016)
IdealGasComponent("oxygen", 0.016)
julia> pressure(oxygen, 500, 300)
7.794375e7
julia> pressure.(Ref(oxygen), 500, [300, 350, 400])
3-element Vector{Float64}:
7.794375e7
9.0934375e7
1.03925e8
Операторы тоже функции. Так, broadcast порождает, например, поэлементные операции над векторами и матрицами (.+, .*, …).
julia> 2 * sin.(x) .* cos.(x) == sin.(2 * x)
true
В этом примере несколько поэлементых видов умножений.
2 * x: умножение скаляра на вектор, оно и без броадкаста действует поэлементно;x .* y: поэлементное умножение векторов.
Если вам надоедает ставить точки везде, воспользуйтесь макросом @.
julia> x = [0, π/6, π/3];
julia> @. 2 * sin(x) * cos(x)
3-element Vector{Float64}:
0.0
0.8660254037844386
0.8660254037844388
Ещё одно важное применение броадкаста: in-place присваивание. Оно делает вашу программу менее прожорливой по памяти, а сборщик мусора не будет добавлять времени работы.
julia> x = rand(100); y = rand(100);
julia> @allocated z = x + y; # первый вызов включает компиляцию
julia> @allocated z = x + y
896
julia> z = similar(x); # выделили память под `z` один раз
julia> @allocated z .= x .+ y; # первый вызов включает компиляцию
julia> @allocated z .= x .+ y
64
На аллокации требуется время, а ещё время требуется для работы сборщика мусора. На данных небольшого размера время сборщика мусора существенно по сравнению с временем исполнения.
Так, сложение двух векторов размера 100 с аллокацией требует в тесте в среднем 85 нс, а с использованием in-place операций 22 нс. Причём разницу, судя по бенчмарку, занимает сборщик мусора (garbage collector, GC).
julia> using BenchmarkTools
julia> x = rand(100); y = rand(100); z = similar(x);
julia> @benchmark $z = $x + $y
BenchmarkTools.Trial: 10000 samples with 973 evaluations.
Range (min … max): 75.194 ns … 619.990 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 78.805 ns ┊ GC (median): 0.00%
Time (mean ± σ): 85.932 ns ± 41.564 ns ┊ GC (mean ± σ): 5.88% ± 9.87%
█▄▁ ▁
█████▇█▇▅▅▅▅▄█▇▄▃▃▅▄▁▄▃▃▁▁▄▄▁▄▁▁▁▁▁▁▁▁▁▁▁▁▁▄▁▁▃▁▃▃▁▁▄▅▆▅▇▆▇▆ █
75.2 ns Histogram: log(frequency) by time 365 ns <
Memory estimate: 896 bytes, allocs estimate: 1.
julia> @benchmark $z .= $x .+ $y
BenchmarkTools.Trial: 10000 samples with 997 evaluations.
Range (min … max): 20.640 ns … 398.402 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 20.930 ns ┊ GC (median): 0.00%
Time (mean ± σ): 22.264 ns ± 8.109 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
█▃▁▁▁ ▁
█████▇▆▆▇▇▆▆▆▆▆▆▆▆▅▅▅▆▆▆▆▆▇▇▇▇▇▇▆▇▇▆▇▇▇▆▅▆▆▆▆▄▄▅▅▄▄▄▄▄▅▃▁▄▁▄ █
20.6 ns Histogram: log(frequency) by time 45 ns <
Memory estimate: 0 bytes, allocs estimate: 0.
Повторим тест для больших векторов, возьмём их размером 1000000.
julia> x = rand(1_000_000); y = rand(1_000_000); z = similar(x);
julia> @benchmark $z = $x + $y
BenchmarkTools.Trial: 2902 samples with 1 evaluation.
Range (min … max): 901.201 μs … 6.733 ms ┊ GC (min … max): 0.00% … 46.36%
Time (median): 1.275 ms ┊ GC (median): 0.00%
Time (mean ± σ): 1.713 ms ± 976.100 μs ┊ GC (mean ± σ): 26.05% ± 25.78%
██▄▁ ▁▄▄▂▁ ▁
▃▁▁▁▁▅████▇▄▇▅▃▃▁▃▁▁▄▃▁▁▁▃▁▃▃▁▃▁▁▁▃▁▁▁▁▁▁▁▁▁▃▃▁▁▁▁▁▁▁▅██████▇ █
901 μs Histogram: log(frequency) by time 470 ms <
Memory estimate: 7.63 MiB, allocs estimate: 2.
julia> @benchmark $z .= $x .+ $y
BenchmarkTools.Trial: 5360 samples with 1 evaluation.
Range (min … max): 789.767 μs … 5.811 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 832.923 μs ┊ GC (median): 0.00%
Time (mean ± σ): 921.733 μs ± 295.907 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▅█▆▃▃▂▁▁▁▁▁▁▁▁ ▁
██████████████████▇█▇▇▆▆▇▆▅▆▅▆▆▅▆▅▅▅▄▄▄▄▄▄▄▃▄▅▅▄▅▅▁▄▃▄▅▄▄▄▄▄▅ █
790 μs Histogram: log(frequency) by time 2.42 ms <
Memory estimate: 0 bytes, allocs estimate: 0.
На векторах размера 100 разница среднего времени исполнения составляла \(\approx 4\) раза, а на больших векторах уменьшилась до \(\approx 2\) раз.