SubArrays
نوع SubArray
في جوليا هو حاوية تشفر "عرض" من والد AbstractArray
. توثق هذه الصفحة بعض مبادئ التصميم وتنفيذ SubArray
s.
أحد الأهداف الرئيسية في التصميم هو ضمان أداء عالي لعرض كل من IndexLinear
و IndexCartesian
المصفوفات. علاوة على ذلك، يجب أن تكون العروض لمصفوفات IndexLinear
هي نفسها IndexLinear
بقدر ما هو ممكن.
Index replacement
اعتبر إنشاء شرائح ثنائية الأبعاد من مصفوفة ثلاثية الأبعاد:
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
s ثنائية الأبعاد. وبالتالي، فإن الطريقة الطبيعية لفهرسة هذه هي باستخدام 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
ومصفوفات منها، انظر أدناه).
هذا هو جوهر 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
متداخلة مع مستويين من التوجيه بدلاً من ذلك.