- Consider the following piece of code that calculates a dot product of two arrays.
function dot_product(a::Array, b::Array)
@assert length(a) == length(b)
sum = 0
for i in 1:length(a)
sum += a[i] * b[i]
end
return sum
end
It is a very simple function. It makes sure that the two arrays are of the same length. And, if yes, it sums the element-wise product.
When you fetch an array item
a[i], a language like Julia needs to make sure that the indexiis within the bounds of the array.In C, trying to access an index out of bound might give a
Segmentation Fault.But since our index
ionly runs through1:length(a), it is guaranteed to be in bound.We can save the program the effort of validating this index. This is done by using the
@inboundsmacro.A new function looks like the following.
function dot_product_with_inbounds(a::Array, b::Array)
@assert length(a) == length(b)
sum = 0
@inbounds for i in 1:length(a)
sum += a[i] * b[i]
end
return sum
end
- The
@inboundsmacro is telling the compiler “Look, I promise that the indexing is correct.” - Since the compiler won’t have to do this check, the resulting code is slightly faster.
- How fast you ask?
using BenchmarkTools
a = rand(0:100, 5000); # random array of length 5000
b = rand(0:100, 5000);
@btime dot_product(a, b)
>> 2.220 μs (1 allocation: 16 bytes)
@btime dot_product_with_inbounds(a, b)
>> 1.034 μs (1 allocation: 16 bytes)
- Using
@inboundsreduced the execution time by a half. That is a huge gain for something so simple.
Being careless with @inbounds
- What happens if you tell the compiler your indexeing is correct but it really isn’t?
- Consider a careless function to do a dot-product like the following
function dot_product_careless(a::Array, b::Array)
@assert length(a) == length(b)
sum = 0
@inbounds for i in 1:length(a)+1 # i is going to go out of bounds
sum += a[i] * b[i]
end
return sum
end
- We have used the
@inboundsmacro butiis clearly going to go out of bounds. - How will the compiler handle this? Any guess?
- If you do not use the
@inboundsoperation and run your code, the compiler is going to return an error when you try to access the out of bound index.
function dot_product_bad(a::Array, b::Array)
@assert length(a) == length(b)
sum = 0
for i in 1:length(a) + 1 # out of bound index
sum += a[i] * b[i]
end
return sum
end
dot_product_bad(a, b)
>>> ERROR: BoundsError: attempt to access 5000-element Array{Int64,1} at index [5001]
>>>Stacktrace:
>>> [1] getindex at ./array.jl:809 [inlined]
>>> [2] dot_product_careless(::Array{Int64,1}, ::Array{Int64,1}) at ./REPL[102]:5
>>> [3] top-level scope at REPL[103]:1
- If you use
@inbounds, the compiler just trusts you and tries to get the value of the out of bound index.
function dot_product_inbounds_bad(a::Array, b::Array)
@assert length(a) == length(b)
sum = 0
@inbounds for i in 1:length(a) + 1 # out of bound index
sum += a[i] * b[i]
end
return sum
end
dot_product_inbounds_bad(a, b)
>>> 1008843416470889372 # a.b should not be this high.
- What happened here is that the program fetched some garbage number sitting at
a[i+1]andb[i+1].
- Moral of the story: Use
@inboundsif you trust yourself into not making careless mistakes.