• 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 index i is 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 i only runs through 1: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 @inbounds macro.

  • 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 @inbounds macro 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 @inbounds reduced 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 @inbounds macro but i is clearly going to go out of bounds.
  • How will the compiler handle this? Any guess?
  • If you do not use the @inbounds operation 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] and b[i+1].

  • Moral of the story: Use @inbounds if you trust yourself into not making careless mistakes.