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\) раз.