`@propagate_inbounds` and `@boundscheck` vs `checkbounds`

checkbounds is intuitive, but how does it compare and work with the other 2? Pulled from the following example in JuAFEM.jl:

@propagate_inbounds function assemble!(g::AbstractVector{T}, edof::AbstractVector{Int}, ge::AbstractVector{T}) where {T}
    @boundscheck checkbounds(g, edof)
    @inbounds for i in 1:length(edof)
        g[edof[i]] += ge[i]
    end
end

checkbounds is a helper function and it doesn’t need to work with the other two.

The semantic of @inbounds, @boundcheck and @propagate_inbounds are pretty well documented.

1 Like

I’m on a phone or I would do it myself, but a link to the relevant documentation would be helpful here.

1 Like

Did you mean https://docs.julialang.org/en/latest/devdocs/boundscheck/ ?

2 Likes

Note: This comment is full of mistakes, that I didn’t even bother correcting it. Please read on for the correct understanding.

So I would like to paraphrase the docs and specialize on the above example, correct me if I am wrong.

It seems that @boundscheck, which should ideally surround only bound checking lines, communicates with the function calling assemble! only if assemble! was inlined and the function call was in an @inbounds block, telling it (or the compiler really) to skip the lines in the @boundscheck block.

@propagate_inbounds looks for @boundscheck blocks in one deeper layer of function calls, skipping any lines in the @boundscheck blocks.

So it is useless to do the following:

  1. Use @propagate_inbounds if no @inbounds block exists in the function. This one is pretty obvious.
  2. Use @boundscheck block in a function when the function is not inlined and directly called in an @inbounds block in the caller function, or is indirectly called in an @inbounds block in a function (any number of function calls down the stack) decorated with @propagate_inbounds.

No. @inbounds only works with one level of @inbounds, @propagate_inbounds extends it.

This is clearly documented as

To override the “one layer of inlining” rule, a function may be marked with @propagate_inbounds to propagate an inbounds context (or out of bounds context) through one additional layer of inlining.

No.

1 Like

I see so only one additional layer, my bad. I will edit my post.

Could you elaborate? How can @propagate_inbounds extend @inbounds and yet still work without it?

One more question, the docs seem to imply that the lines in an @bounds_check block are only skipped if the function having that block is inlined? Does this mean that an @inline decorator is missing from the above example for the @bounds_check to be of any use?

@propagate_inbounds propagate @inbounds into the function. It has nothing to do with @inbounds in the function. @inbounds is also independent of anything of the function it is in.

yes.

no. please read the docstring

help?> Base.@propagate_inbounds
  @propagate_inbounds

  Tells the compiler to inline a function while retaining the caller's inbounds context.

Oh, ok so @propagate_inbounds inlines the function and extends the caller function’s @inbounds context, it has nothing to do with the functions called by assemble! for example. So a simple @inline extends the @inbounds context only once. For additional extensions, @propagate_inbounds is necessary. So if f1 calls f2 in an @inbounds block, and f2 calls f3 without @inbounds, then inlining f2 will extend the @inbounds of f1, but using @propagate_inbounds is necessary before f3 for the @inbounds context of f1 to be passed to f3 through f2. A side effect of this is that f3 will be inlined in f2 which itself is inlined in f1. Alternatively, f2 could have been decorated with @propagate_inbounds instead of @inline, as the inlining happens anyways. Is this about right?

I think this part is worth adding to the docs link.

help?> Base.@propagate_inbounds
  @propagate_inbounds

  Tells the compiler to inline a function while retaining the caller's inbounds context.