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
.