Подмассивы (SubArrays)
Тип SubArray
Julia — это контейнер, кодирующий представление родительского типа AbstractArray
. На этой странице описаны некоторые принципы проектирования и реализации подмассивов (SubArray
).
Одной из основных целей проектирования является обеспечение высокой производительности для представлений массивов типов IndexLinear
и IndexCartesian
. Более того, представления массивов IndexLinear
сами должны иметь тип IndexLinear
, насколько это возможно.
Замена индекса
Рассмотрим возможность создания двумерных срезов трехмерного массива.
julia> A = rand(2,3,4);
julia> S1 = view(A, :, 1, 2:3)
2×2 view(::Array{Float64, 3}, :, 1, 2:3) with eltype Float64:
0.839622 0.711389
0.967143 0.103929
julia> S2 = view(A, 1, :, 2:3)
3×2 view(::Array{Float64, 3}, 1, :, 2:3) with eltype Float64:
0.839622 0.711389
0.789764 0.806704
0.566704 0.962715
view
отбрасывает отдельные измерения (те, которые задаются с помощью Int
), поэтому S1
и S2
являются двумерными подмассивами (SubArray
). Следовательно, естественным способом их индексирования является использование S1[i,j]
. Чтобы извлечь значение из родительского массива A
, можно воспользоваться естественным подходом и заменить S1[i,j]
на A[i,1,(2:3)[j]]
и S2[i,j]
на A[1,i,(2:3)[j]]
.
Ключевой особенностью проектирования подмассивов является то, что эта замена индекса может быть выполнена без каких-либо затрат во время выполнения.
Проектирование подмассива
Параметры типов и поля
Принятая стратегия выражается, прежде всего, в определении типа:
struct SubArray{T,N,P,I,L} <: AbstractArray{T,N}
parent::P
indices::I
offset1::Int # для линейного индексирования и указателя, допустимо только при L==true
stride1::Int # используется только для линейного индексирования
...
end
Подмассив (SubArray
) имеет пять параметров типа. Первые два — это стандартный тип и размерность элемента. Следующим является тип родительского AbstractArray
. Наиболее часто используемым является четвертый параметр, представляющий собой кортеж (Tuple
) типов индексов для каждого измерения. Последний, L
, предоставляется только для удобства диспетчеризации. Это логическое значение, которое определяет, поддерживают ли типы индексов быстрое линейное индексирование. Более подробно об этом позже.
Если в примере выше A
является Array{Float64, 3}
, то в нашем случае S1
будет иметь значение SubArray{Float64,2,Array{Float64,3},Tuple{Base.Slice{Base.OneTo{Int64}},Int64,UnitRange{Int64}},false}
. Обратите внимание, в частности, на параметр кортежа, который хранит типы индексов, используемых для создания S1
. Аналогичным образом:
julia> S1.indices
(Base.Slice(Base.OneTo(2)), 1, 2:3)
Хранение этих значений позволяет заменять индексы, а наличие типов, закодированных в виде параметров, позволяет отправлять их в эффективные алгоритмы.
Преобразование индексов
Для преобразования индексов требуется выполнять разные действия для разных конкретных типов подмассива (SubArray
). Например, для S1
нужно применить индексы i,j
к первому и третьему измерениям родительского массива, а для S2
их нужно применить ко второму и третьему. Проще всего выполнить индексирование можно было бы путем анализа типов во время выполнения.
parentindices = Vector{Any}()
for thisindex in S.indices
...
if isa(thisindex, Int)
# Не использовать ни один из входных индексов
push!(parentindices, thisindex)
elseif isa(thisindex, AbstractVector)
# Использовать входной индекс
push!(parentindices, thisindex[inputindex[j]])
j += 1
elseif isa(thisindex, AbstractMatrix)
# Использовать два входных индекса
push!(parentindices, thisindex[inputindex[j], inputindex[j+1]])
j += 2
elseif ...
end
S.parent[parentindices...]
К сожалению, это было бы катастрофично с точки зрения производительности: при каждом доступе к элементу будет выделяться память, что повлечет за собой выполнение большого объема плохо типизированного кода.
Лучшим подходом является диспетчеризация в определенные методы для обработки каждого типа хранимого индекса. Именно это и делает функция reindex
: она выполняет диспетчеризацию типа первого хранимого индекса и использует соответствующее количество входных индексов, а затем выполняет рекурсию по оставшимся индексам. В случае с S1
она расширяется до следующего вида:
Base.reindex(S1, S1.indices, (i, j)) == (i, S1.indices[2], S1.indices[3][j])
для любой пары индексов (i,j)
(кроме декартовых индексов (CartesianIndex
) и их массивов, см. ниже).
В этом заключается суть подмассива (SubArray
). Методы индексирования зависят от функции reindex
для реализации такого преобразования индексов. Но иногда мы можем обойтись без косвенных действий и выполнить преобразование еще быстрее.
Линейное индексирование
Линейное индексирование может быть эффективно реализовано в случае, когда весь массив имеет один шаг, который разделяет последовательные элементы, начиная с некоторого смещения. Это означает, что мы можем предварительно вычислить эти значения и представить линейное индексирование просто как сложение и умножение, полностью исключив косвенное действие функции reindex
и (что более важно) медленное вычисление декартовых координат.
Для типов SubArray
доступность эффективного линейного индексирования основана исключительно на типах индексов и не зависит от таких значений, как размер родительского массива. Вы можете узнать, поддерживает ли данный набор индексов быстрое линейное индексирование, с помощью внутренней функции Base.viewindexing
.
julia> Base.viewindexing(S1.indices)
IndexCartesian()
julia> Base.viewindexing(S2.indices)
IndexLinear()
Результат вычисляется во время построения подмассива (SubArray
) и хранится в параметре типа L
в виде логического значения, которое кодирует поддержку быстрого линейного индексирования. Не являясь строго обязательным, это означает, что можно определить диспетчеризацию непосредственно в SubArray{T,N,A,I,true}
без каких-либо посредников.
Поскольку это вычисление не зависит от значений времени выполнения, оно может пропускать некоторые случаи, когда шаг массива оказывается равномерным.
julia> A = reshape(1:4*2, 4, 2)
4×2 reshape(::UnitRange{Int64}, 4, 2) with eltype Int64:
1 5
2 6
3 7
4 8
julia> diff(A[2:2:4,:][:])
3-element Vector{Int64}:
2
2
2
Представление, построенное как view(A, 2:2:4, :)
, имеет равномерный шаг массива, и поэтому линейное индексирование действительно может быть выполнено эффективно. Однако успех в этом случае зависит от размера массива: если бы первое измерение было нечетным,
julia> A = reshape(1:5*2, 5, 2)
5×2 reshape(::UnitRange{Int64}, 5, 2) with eltype Int64:
1 6
2 7
3 8
4 9
5 10
julia> diff(A[2:2:4,:][:])
3-element Vector{Int64}:
2
3
2
в A[2:2:4,:]
не было бы равномерного шага, поэтому мы не могли гарантировать эффективную линейную индексацию. Поскольку это решение должно основываться исключительно на типах, закодированных в параметрах подмассива (SubArray
), S = view(A, 2:2:4, :)
не может реализовать эффективную линейную индексацию.
Несколько деталей
-
Обратите внимание, что функция
Base.reindex
не зависит от типов входных индексов. Она просто определяет, как и где хранящиеся индексы должны быть переиндексированы. Она поддерживает не только целочисленные индексы, но и нескалярное индексирование. Это означает, что представлениям представлений не нужны два уровня косвенных действий. Они могут просто пересчитать индексы в исходный родительский массив. -
Возможно, теперь уже достаточно ясно, что поддержка срезов означает, что размерность, задаваемая параметром
N
, необязательно равна размерности родительского массива или длине кортежаindices
. Указанные пользователем индексы также необязательно совпадают с записями в кортежеindices
(например, второй указанный пользователем индекс может соответствовать третьему измерению родительского массива и третьему элементу в кортежеindices
).Менее очевидным может быть тот факт, что размерность хранимого родительского массива должна быть равна количеству эффективных индексов в кортеже
indices
. Некоторые примеры:A = reshape(1:35, 5, 7) # Двумерный родительский массив S = view(A, 2:7) # Одномерное представление, созданное с помощью линейного индексирования S = view(A, :, :, 1:1) # Поддерживается добавление дополнительных индексов
Наивно полагать, что можно просто задать
S.parent = A
иS.indices = (:,:,1:1)
, но поддержка такого выражения значительно усложняет процесс переиндексации, особенно для представлений представлений. Вам нужно не только выполнять диспетчеризацию типов хранимых индексов, но и проверять, является ли заданный индекс конечным, и объединять все оставшиеся хранимые индексы. Это нелегкая задача. Более того, она медленная, поскольку неявно зависит от линейного индексирования.К счастью, это именно то вычисление, которое выполняет массив
ReshapedArray
, и при возможности он делает это линейно. Следовательно, представление (view
) гарантирует, что родительский массив имеет соответствующую размерность для заданных индексов, при необходимости изменяя его форму. Внутренний конструкторSubArray
обеспечивает выполнение этого инварианта. -
CartesianIndex
и его массивы вносят неприятные коррективы в схемуreindex
. Вспомним, что функцияreindex
просто выполняет диспетчеризацию типов хранимых индексов, чтобы определить, сколько переданных индексов должно быть использовано и где. Но декартов индекс (CartesianIndex
) не поддерживает однозначное соответствие между количеством передаваемых аргументов и количеством измерений, в которые они индексируются. Если мы вернемся к приведенному выше примеруBase.reindex(S1, S1.indices, (i, j))
, мы увидим, что расширение неверно дляi, j = CartesianIndex(), CartesianIndex(2,1)
. Оно должно полностью пропуститьCartesianIndex()
и возвратить следующее.(CartesianIndex(2,1)[1], S1.indices[2], S1.indices[3][CartesianIndex(2,1)[2]])
Однако вместо этого мы получаем:
(CartesianIndex(), S1.indices[2], S1.indices[3][CartesianIndex(2,1)])
При правильном подходе потребуется выполнить комбинированную диспетчеризацию как хранимых, так и передаваемых индексов во всех сочетаниях размерностей сложным способом. Поэтому функция
reindex
никогда не должна вызываться с индексамиCartesianIndex
. К счастью, со скалярным случаем легко справиться, сначала разложив аргументыCartesianIndex
на простые целые числа. Однако массивыCartesianIndex
невозможно так легко разделить на независимые части. Прежде чем попытаться использовать функциюreindex
, представление (view
) должно убедиться, что в списке аргументов нет массивовCartesianIndex
. Если они есть, оно может просто полностью исключить вычислениеreindex
, построив вместо него вложенный подмассив (SubArray
) с двумя уровнями косвенности.