Julia: @inbounds and @propagate_inbounds

Explore @inbounds and @propagate_inbounds in Julia.

Background

I am developing a package recently, which needs to create some custom matrices. When designing the getindex function, I need to add code to do bounds checking and provide a way to skip bounds checking.

The official documentation discusses this topic in detail, including how to use @inbounds and @propagate_inbounds, but it is quite brief and does not provide example code. This article will demonstrate how to use these two macros and their performance differences.

@inbounds

When we try to access a out-of-bound index, Julia will throw a BoundsError exception, we can use @inbounds to skip the bounds check.

1
2
3
4
5
6
7
8
9
10
@inline function g(A, i)
@boundscheck checkbounds(A, i)
return "accessing ($A)[$i]"
end

function test()
g(1:2, -1)
end

test() # ERROR: BoundsError: attempt to access 2-element UnitRange{Int64} at index [-1]

Add @inbounds when calling g to skip the bounds check.

1
2
3
4
5
function test()
@inbounds g(1:2, -1)
end

test() # "accessing (1:2)[-1]"

Multple layers check: @propagate_inbounds

Problem

If a function has multiple layers of bounds checking, the outermost @inbounds can only skip the outermost bounds checking, and the inner bounds checking will still be executed.

1
2
3
4
5
6
7
8
9
10
@inline function f(A, i)
@boundscheck checkbounds(A, i)
return g(1:2, -1)
end

function test()
@inbounds f(1:2, -1)
end

test() # ERROR: BoundsError: attempt to access 2-element UnitRange{Int64} at index [-1]

Classic Solution

Update the f function, add @inbounds in each function call to skip the bounds check.

1
2
3
4
5
6
@inline function f(A, i)
@boundscheck checkbounds(A, i)
return @inbounds g(1:2, -1)
end

test() # "accessing (1:2)[-1]"

In Julia official library LinearAlgebra.jl, @inbounds is widely used, for example, the Tridiagonal matrix:

The solution used in the official library

@propagate_inbounds

@propagate_inbounds can help us solve the problem of needing to add @inbounds to each function call in multiple layers of calls. It can be used to propagate the context of bounds checking, allowing us to skip bounds checking in multiple layers of function calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@inline function g(A, i)
@boundscheck checkbounds(A, i)
return "accessing ($A)[$i]"
end

@inline Base.@propagate_inbounds function f(A, i)
@boundscheck checkbounds(A, i)
return g(1:2, -1)
end

function test()
@inbounds f(1:2, -1)
end

test() # "accessing (1:2)[-1]"

Therefor, f can pass the information of skipping bounds checking to g, as long as @inbounds is added when calling f, the inner bounds checking can be skipped.

It should be noted that @propagate_inbounds will not be propagated indefinitely. If the g function calls other functions with bounds checking, @propagate_inbounds or @inbounds should be used.

Performance Differences

Next, we will test the performance differences between @inbounds and @propagate_inbounds. The test code is as follows, f1 uses @inbounds, f2 uses @propagate_inbounds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using BenchmarkTools

@inline function f1(A, i)
@boundscheck checkbounds(A, i)
return @inbounds g(1:2, -1)
end

@inline Base.@propagate_inbounds function f2(A, i)
@boundscheck checkbounds(A, i)
return g(1:2, -1)
end

@inline function g(A, i)
@boundscheck checkbounds(A, i)
return "g: accessing ($A)[$i]"
end

function test_f1()
for i = 1:100000
@inbounds f1(1:2, -1)
end
end

function test_f2()
for i = 1:100000
@inbounds f2(1:2, -1)
end
end

function main()
@inbounds f1(1:2, -1)
@inbounds f2(1:2, -1)

t1 = @benchmark test_f1()
t2 = @benchmark test_f2()
display(t1)
display(t2)
end

We can say that there is no performance difference between the two.

Test results

Conclusion

@inbounds can skip the bounds check of a single layer of function calls, which can be used when calling the outermost function, helping to improve performance, but also have pay attention to the security of access.

@propagate_inbounds can skip the bounds check of multiple layers of function calls, which is suitable for the development of packages. This macro can provide better code cleanliness, avoiding the trouble of adding @inbounds to each layer of function calls.

Comprehensively, there is no performance difference between the two, and using @propagate_inbounds is a better choice when developing packages.


Julia: @inbounds and @propagate_inbounds
https://blog.zhanganzhi.com/en/2024/08/7df2e472657e/
Author
Andy Zhang
Posted on
August 26, 2024
Licensed under