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