Документация Engee

Проверка границ

Как и многие современные языки программирования, Julia использует проверку границ для обеспечения безопасности программы при доступе к массивам. В сплошных внутренних циклах или других ситуациях, критичных для производительности, проверки границ можно пропустить, чтобы улучшить производительность во время выполнения. Например, для выполнения векторизованных (SIMD) инструкций тело цикла не может содержать ветви, а значит, не может содержать проверки границ. В связи с этим Julia содержит макрос @inbounds(...), указывающий компилятору пропускать такие проверки границ в заданном блоке. Определяемые пользователем типы массивов могут использовать макрос @boundscheck(...) для зависимого от контекста выбора кода.

Обход проверок границ

Макрос @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, расположенные не более чем на один уровень встраивания ниже. Это правило предотвращает внесение непреднамеренных изменений в поведение программы со стороны кода, расположенного далее в стеке.

Внимание!

С помощью @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 # Несогласованные результаты или аварийное завершение

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

Распространение внутри границ

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

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

Иерархия вызовов проверки границ

Общая иерархия выглядит следующим образом:

  • функция 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 предназначена для специализации только типа индекса (особенно последний аргумент).

Выполнение проверок границ

Julia можно запустить с помощью команды --check-bounds={yes|no|auto}, чтобы выполнять проверки границ всегда, никогда или с учетом объявления @inbounds.