No measurable difference means that the compiler was able to prove that all accesses are inbounds and thus remove the runtime check even when you didn’t ask for it. To observe the difference you need to hide the index values better from the compiler, for example like this:
using BenchmarkTools
function add_one!(x, indices)
for i in indices
x[i] += 1
end
end
function add_one_inbounds!(x::AbstractVector, indices::StepRange)
xifirst, xilast = firstindex(x), lastindex(x)
ifirst, ilast = first(indices), last(indices)
if !(xifirst <= ifirst <= xilast) || !(xifirst <= ilast <= xilast)
throw(BoundsError(x, indices))
end
for i in indices
@inbounds x[i] += 1
end
end
function main()
x = rand(1_000_000)
iref = Ref(1:2:1_000_000)
println("Time taken for non-inbounds version:")
@btime add_one!($x, ($iref)[])
println("\nTime taken for inbounds version:")
@btime add_one_inbounds!($x, ($iref)[])
end
main()
julia> main()
Time taken for non-inbounds version:
446.960 μs (0 allocations: 0 bytes)
Time taken for inbounds version:
405.824 μs (0 allocations: 0 bytes)
In other words, @inbounds
is useful in cases where you’re smarter than the compiler, that is, you know that all accesses are inbounds even though the compiler can’t see it.
It’s important to only use @inbounds
in contexts where you know it’s never incorrect, otherwise you could easily trigger segfaults or worse. Hence the manual bounds checks at the top of add_one_inbounds!
, as well type constraints to match the assumptions in those bounds checks.