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.