Bounds checking

Как и многие современные языки программирования, Julia использует проверку границ для обеспечения безопасности программы при доступе к массивам. В узких внутренних циклах или в других ситуациях, критичных к производительности, вы можете захотеть пропустить эти проверки границ, чтобы улучшить производительность во время выполнения. Например, для того чтобы сгенерировать векторизованные (SIMD) инструкции, тело вашего цикла не может содержать ветвления и, следовательно, не может содержать проверки границ. В результате Julia включает макрос @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

Который тихо предполагает индексацию с единицы и, следовательно, подвергает небезопасному доступу к памяти при использовании с OffsetArrays:

julia> using OffsetArrays

julia> sum(OffsetArray([1, 2, 3], -10))
9164911648 # inconsistent results or segfault

Хотя оригинальным источником ошибки здесь является 1:length(A), использование @inbounds увеличивает последствия от ошибки выхода за границы до менее легко уловимого и отлаживаемого небезопасного доступа к памяти. Часто трудно или невозможно доказать, что метод, использующий @inbounds, безопасен, поэтому необходимо взвесить преимущества улучшения производительности против риска сегментационных ошибок и тихого неправильного поведения, особенно в публичных API.

Propagating inbounds

Могут быть определенные сценарии, когда по причинам организации кода вы хотите иметь более одного уровня между объявлениями @inbounds и @boundscheck. Например, методы по умолчанию getindex имеют цепочку getindex(A::AbstractArray, i::Real) вызывает getindex(IndexStyle(A), A, i) вызывает _getindex(::IndexLinear, A, i).

Чтобы переопределить правило "одного уровня инлайнинга", функция может быть помечена как Base.@propagate_inbounds, чтобы передать контекст inbounds (или контекст out of bounds) через один дополнительный уровень инлайнинга.

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.