Bounds checking
مثل العديد من لغات البرمجة الحديثة، تستخدم جوليا فحص الحدود لضمان سلامة البرنامج عند الوصول إلى المصفوفات. في الحلقات الداخلية الضيقة أو في حالات الأداء الحرجة الأخرى، قد ترغب في تخطي هذه الفحوصات لتحسين أداء وقت التشغيل. على سبيل المثال، من أجل إصدار تعليمات متجهة (SIMD)، لا يمكن أن يحتوي جسم الحلقة على فروع، وبالتالي لا يمكن أن يحتوي على فحوصات الحدود. وبالتالي، تتضمن جوليا ماكرو @inbounds(...)
لإخبار المترجم بتخطي مثل هذه الفحوصات ضمن الكتلة المعطاة. يمكن لأنواع المصفوفات المعرفة من قبل المستخدم استخدام ماكرو @boundscheck(...)
لتحقيق اختيار كود حساس للسياق.
Eliding bounds checks
تحدد الماكرو @boundscheck(...)
كتل الشيفرة التي تقوم بإجراء فحص الحدود. عندما يتم تضمين مثل هذه الكتل في كتلة @inbounds(...)
، قد يقوم المترجم بإزالة هذه الكتل. يقوم المترجم بإزالة كتلة @boundscheck
فقط إذا تم تضمينها في الدالة المستدعية. على سبيل المثال، يمكنك كتابة الدالة sum
كالتالي:
function sum(A::AbstractArray)
r = zero(eltype(A))
for i in eachindex(A)
@inbounds r += A[i]
end
return r
end
مع نوع مخصص يشبه المصفوفة MyArray
يحتوي على:
@inline getindex(A::MyArray, i::Real) = (@boundscheck checkbounds(A, i); A.data[to_index(i)])
ثم عندما يتم تضمين getindex
في sum
، سيتم حذف الاستدعاء إلى checkbounds(A, i)
. إذا كانت دالتك تحتوي على عدة طبقات من التضمين، فإن كتل @boundscheck
يتم إزالتها في أقصى حد لمستوى واحد من التضمين الأعمق. القاعدة تمنع التغييرات غير المقصودة في سلوك البرنامج من التعليمات البرمجية الموجودة أعلى المكدس.
Caution!
من السهل أن تكشف عن عمليات غير آمنة عن طريق الخطأ باستخدام @inbounds
. قد تشعر بالإغراء لكتابة المثال أعلاه كـ
function sum(A::AbstractArray)
r = zero(eltype(A))
for i in 1:length(A)
@inbounds r += A[i]
end
return r
end
الذي يفترض به بهدوء الفهرسة التي تبدأ من 1 وبالتالي يكشف عن وصول غير آمن للذاكرة عند استخدامه مع OffsetArrays
:
julia> using OffsetArrays
julia> sum(OffsetArray([1, 2, 3], -10))
9164911648 # inconsistent results or segfault
بينما المصدر الأصلي للخطأ هنا هو 1:length(A)
، فإن استخدام @inbounds
يزيد من العواقب من خطأ الحدود إلى وصول غير آمن للذاكرة أقل سهولة في اكتشافه وتصحيحه. غالبًا ما يكون من الصعب أو المستحيل إثبات أن طريقة تستخدم @inbounds
آمنة، لذا يجب على المرء أن يوازن بين فوائد تحسين الأداء مقابل خطر حدوث أخطاء في الوصول إلى الذاكرة وسلوكيات صامتة غير صحيحة، خاصة في واجهات برمجة التطبيقات العامة.
Propagating inbounds
قد تكون هناك سيناريوهات معينة حيث لأسباب تتعلق بتنظيم الشيفرة، ترغب في وجود أكثر من طبقة واحدة بين إعلانات @inbounds
و @boundscheck
. على سبيل المثال، طرق getindex
الافتراضية تحتوي على السلسلة getindex(A::AbstractArray, i::Real)
تستدعي getindex(IndexStyle(A), A, i)
تستدعي _getindex(::IndexLinear, A, i)
.
لتجاوز قاعدة "طبقة واحدة من التضمين"، يمكن وضع علامة على الدالة بـ Base.@propagate_inbounds
لنشر سياق ضمن الحدود (أو سياق خارج الحدود) من خلال طبقة إضافية واحدة من التضمين.
The bounds checking call hierarchy
الهيكل العام هو:
checkbounds(A, I...)
الذي يستدعيcheckbounds(Bool, A, I...)
الذي يستدعيcheckbounds_indices(Bool, axes(A), I)
الذي يستدعي بشكل متكررcheckindex
لكل بُعد
هنا A
هو المصفوفة، و I
تحتوي على "المؤشرات" "المطلوبة". axes(A)
تُرجع مجموعة من "المؤشرات" "المسموح بها" لـ A
.
checkbounds(A, I...)
يُثير خطأ إذا كانت الفهارس غير صالحة، بينما checkbounds(Bool, A, I...)
تُرجع false
في تلك الحالة. checkbounds_indices
تتجاهل أي معلومات عن المصفوفة بخلاف مجموعة axes
الخاصة بها، وتقوم بإجراء مقارنة نقية بين الفهارس: هذا يسمح لعدد قليل نسبيًا من الطرق المجمعة بخدمة مجموعة كبيرة من أنواع المصفوفات. يتم تحديد الفهارس كأزواج، وعادةً ما يتم مقارنتها بطريقة 1-1 مع الأبعاد الفردية التي يتم التعامل معها عن طريق استدعاء دالة مهمة أخرى، checkindex
: عادةً،
checkbounds_indices(Bool, (IA1, IA...), (I1, I...)) = checkindex(Bool, IA1, I1) &
checkbounds_indices(Bool, IA, I)
لذا checkindex
يتحقق من بُعد واحد. جميع هذه الدوال، بما في ذلك checkbounds_indices
غير المصدرة، تحتوي على وثائق يمكن الوصول إليها باستخدام ؟
.
إذا كان عليك تخصيص فحص الحدود لنوع مصفوفة محدد، يجب عليك تخصيص checkbounds(Bool, A, I...)
. ومع ذلك، في معظم الحالات يجب أن تكون قادرًا على الاعتماد على checkbounds_indices
طالما أنك تزود axes
مفيدة لنوع مصفوفاتك.
إذا كان لديك أنواع فهرسة جديدة، فكر أولاً في تخصيص checkindex
، الذي يتعامل مع فهرس واحد بعد معين لمصفوفة. إذا كان لديك نوع فهرس متعدد الأبعاد مخصص (مشابه لـ CartesianIndex
)، فقد تحتاج إلى التفكير في تخصيص checkbounds_indices
.
لاحظ أن هذه الهيكلية قد تم تصميمها لتقليل احتمالية الغموض في الطرق. نحن نحاول جعل checkbounds
هو المكان الذي يتم فيه التخصص على نوع المصفوفة، ونسعى لتجنب التخصصات على أنواع الفهارس؛ وعلى العكس، فإن checkindex
يهدف إلى أن يكون متخصصًا فقط على نوع الفهرس (خصوصًا، الوسيطة الأخيرة).
Emit bounds checks
يمكن إطلاق Julia باستخدام --check-bounds={yes|no|auto}
لإصدار فحوصات الحدود دائمًا، أو أبدًا، أو احترام إعلانات @inbounds
.