Keyword arguments preventing eliding boundscheck

So I’ve got a MWE here with a weird behavior.
Note: this seems to be actually fixed in v1.9. But I couldn’t track any relevant issues to PR.

I’m trying to elide some checking code with @boundscheck. (yes, the real use case is a numerical hot loop)

At first I was experimenting with positional arguments:

f2(x, a=0) = @boundscheck "not elided!"
Base.@propagate_inbounds f3(x, a=0) = @boundscheck "not elided!"

function caller(x)
    @inbounds @show f2(x)
    @inbounds @show f2(x, 2)

    @inbounds @show f3(x)
    @inbounds @show f3(x, 2)
    nothing
end
caller(0)

# f2(x) = "not elided!"
# f2(x, 2) = nothing
# f3(x) = nothing
# f3(x, 2) = nothing

After I figured out how to work with @inbounds and @boundscheck properly, I faced some trouble with optional arguments (f2).
But I figured it was because f2(x) was calling f2(x, a) without the “inbound-dedness” so f2(x) boundschecks was not elided. This was fixed with propagate_inbounds.


But now when I also found out the hard way that a similar thing appears to happen with keyword arguments and is not easily fixed.


f4(x; k=0) = @boundscheck "not elided!"
@propagate_inbounds f5(x; k=0) = @boundscheck "not elided!"

function caller(x)
    @inbounds @show f4(x)
    @inbounds @show f4(x, k=2)

    @inbounds @show f5(x)
    @inbounds @show f5(x, k=2)
    nothing
end
caller(0)

# f4(x) = "not elided!"
# f4(x, k = 2) = "not elided!"
# f5(x) = nothing
# f5(x, k = 2) = "not elided!"

Does anyone know of a possible cause for specifying keyword argument to prevent proper inlining or skipping boundchecks?
Or maybe there is a common workaround?
I’ve tried removing the default value of 2 and/or specifying type k::Int64 but neither changes the behavior.

@code_typed does show very obviously the difference between v1.8.5 and v1.9
# V1.8.5
julia> @code_typed f5(1, k=2)
CodeInfo(
1 ─      nothing::Nothing
│        nothing::Nothing
│        nothing::Nothing
└──      goto #3 if not true
2 ─      goto #4
3 ─      goto #4
4 ┄ %7 = φ (#2 => "not ellided!!!", #3 => nothing)::Union{Nothing, String}
└──      return %7
) => Union{Nothing, String}

# v1.9
julia> @code_typed f5(1, k=2)
CodeInfo(
1 ─      nothing::Nothing
│        nothing::Nothing
│        nothing::Nothing
└──      goto #3 if not $(Expr(:boundscheck))
2 ─      goto #4
3 ─      goto #4
4 ┄ %7 = φ (#2 => "not ellided!!!", #3 => nothing)::Union{Nothing, String}
└──      return %7
) => Union{Nothing, String}

Keyword arguments get handled in an auto-generated keyword sorting function that translates a keyword-using function call to a purely positional function call.

julia> foo(;x) = stacktrace()
foo (generic function with 1 method)

julia> foo(;x=3)
15-element Vector{Base.StackTraces.StackFrame}:
 #foo#12 at REPL[25]:1 [inlined]
 (::var"#foo##kw")(::NamedTuple{(:x,), Tuple{Int64}}, ::typeof(foo)) at REPL[25]:1
 top-level scope at REPL[26]:1
 ...

In this example, (::var"#foo##kw") is the keyword sorter and #foo#12 is the keyword-less internal function that it maps to.

I’m guessing that the keyword sorter was not propagating the @inbounds. Unfortunately, I have no idea how you might fix this locally. You might need to use v1.9, since you say it’s fixed there.

I see …
I’ve seen some mentions of that sorter function in the docs.

regarding the specific explanation tho: defining with @noinline shows foo as foo(; x::Int64)…which I think is just my normal method, right? not a keyword-less internal version.

julia> @noinline foo(;x) = stacktrace();

julia> foo(x=1)
15-element Vector{Base.StackTraces.StackFrame}:
 foo(; x::Int64) at REPL[17]:1
 (::var"#foo##kw")(::NamedTuple{(:x,), Tuple{Int64}}, ::typeof(foo)) at REPL[17]:1
 top-level scope at REPL[18]:1
 eval at boot.jl:373 [inlined]
...

Unfortunately I won’t be able to use 1.9 for quite some time for IT bureaucratic reasons.

Your explanation makes sense though, I’ll have to live with it.
I guess that is one of the remaining reasons keyword args aren’t commonly used in numerical code.