SubArrays
Тип SubArray
в Julia является контейнером, который кодирует "представление" родительского AbstractArray
. Эта страница документирует некоторые из принципов проектирования и реализации SubArray
.
Одной из основных целей проектирования является обеспечение высокой производительности для представлений как IndexLinear
, так и IndexCartesian
массивов. Более того, представления массивов IndexLinear
должны сами быть IndexLinear
в той мере, в какой это возможно.
Index replacement
Рассмотрите возможность создания 2d срезов 3d массива:
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]]
.
Ключевой особенностью дизайна SubArrays является то, что эта замена индекса может быть выполнена без каких-либо накладных расходов во время выполнения.
SubArray design
Type parameters and fields
Стратегия, принятая в первую очередь, выражается в определении типа:
struct SubArray{T,N,P,I,L} <: AbstractArray{T,N}
parent::P
indices::I
offset1::Int # for linear indexing and pointer, only valid when L==true
stride1::Int # used only for linear indexing
...
end
SubArray
имеет 5 параметров типа. Первые два - это стандартный тип элемента и размерность. Следующий - это тип родительского 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)
Хранение этих значений позволяет замену индексов, а наличие типов, закодированных в качестве параметров, позволяет вызывать эффективные алгоритмы.
Index translation
Выполнение перевода индексов требует выполнения различных действий для различных конкретных типов SubArray
. Например, для S1
необходимо применить индексы i,j
к первому и третьему измерениям родительского массива, тогда как для S2
их нужно применить ко второму и третьему. Самый простой подход к индексации заключается в том, чтобы проводить анализ типа во время выполнения:
parentindices = Vector{Any}()
for thisindex in S.indices
...
if isa(thisindex, Int)
# Don't consume one of the input indices
push!(parentindices, thisindex)
elseif isa(thisindex, AbstractVector)
# Consume an input index
push!(parentindices, thisindex[inputindex[j]])
j += 1
elseif isa(thisindex, AbstractMatrix)
# Consume two input indices
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
и массивов thereof, см. ниже).
Это основа SubArray
; методы индексации зависят от reindex
, чтобы выполнить эту трансляцию индекса. Однако иногда мы можем избежать косвенной ссылки и сделать это еще быстрее.
Linear indexing
Линейная индексация может быть эффективно реализована, когда весь массив имеет одну единственную шагу, которая отделяет последовательные элементы, начиная с некоторого смещения. Это означает, что мы можем заранее вычислить эти значения и представить линейную индексацию просто как сложение и умножение, избегая косвенной индексации 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, :)
не может реализовать эффективную линейную индексацию.
A few details
Обратите внимание, что функция
Base.reindex
не зависит от типов входных индексов; она просто определяет, как и где должны быть переиндексированы сохраненные индексы. Она поддерживает не только целочисленные индексы, но и нескалярное индексирование. Это означает, что представления представлений не нуждаются в двух уровнях косвенности; они могут просто пересчитывать индексы в исходный родительский массив!Надеюсь, к этому моменту стало довольно ясно, что поддержка срезов означает, что размерность, заданная параметром
N
, не обязательно равна размерности родительского массива или длине кортежаindices
. Также предоставленные пользователем индексы не обязательно совпадают с записями в кортежеindices
(например, второй индекс, предоставленный пользователем, может соответствовать третьему измерению родительского массива и третьему элементу в кортежеindices
).Что может быть менее очевидным, так это то, что размерность хранимого родительского массива должна быть равна количеству эффективных индексов в кортеже
indices
. Вот несколько примеров:A = reshape(1:35, 5, 7) # A 2d parent Array S = view(A, 2:7) # A 1d view created by linear indexing S = view(A, :, :, 1:1) # Appending extra indices is supported
Наивно было бы думать, что вы просто можете установить
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
с двумя уровнями косвенности вместо этого.