Speed of `hasmethod` with and without kwargs

I’m a novice at writing high-performance code with Julia (1.11).

I have an unknown function from a user, and I want to check if it accepts any of several possible sets of keyword names. But for reasons unknown to me, hasmethod is very slow when applied to kwargs:

test_no_kw(a) = 1
test_kw(; a) = 2
test_many_kw(; a, b, c) = 3

@btime hasmethod(test_no_kw, (Any,)) # 2 ns
@btime hasmethod(test_kw, (), (:a,)) # 747 ns
@btime hasmethod(test_many_kw, (), (:a, :c, :b)) # 782 ns

What exactly is going on under the hood that makes the second call so much slower (and the third call not much slower than that)? Should I avoid checking for keywords like this entirely if I need to write fast code, or is there a workaround?

2ns is currently around the benchmark results of integer addition, an indication that the actual call got compiled out of the benchmark loop. Use $ for runtime arguments in BenchmarkTools, it changes quite a bit:

julia> @btime hasmethod(test_no_kw, (Any,))
  1.000 ns (0 allocations: 0 bytes)
true

julia> @btime hasmethod($test_no_kw, $(Any,))
  625.444 ns (9 allocations: 352 bytes)
true

Still slower than involving keyword arguments, I imagine it’s just extra work after method dispatch over the positional arguments.

julia> @btime hasmethod($test_many_kw, $(), $(:a,))
  1.240 μs (15 allocations: 568 bytes)
true

julia> @btime hasmethod($test_many_kw, $(), $(:a, :c, :b))
  1.260 μs (15 allocations: 600 bytes)
true

Ideally you’re not checking for method existence at runtime.

2 Likes

I think that’s a realistic scenario if one wants to use hasmethod inside a function (with constant keyword names).

Apart from speed, I don’t think that using hasmethod is a good idea in this case. The reason is that it cannot deal with functions accepting a variable number of keyword arguments. Imagine the following pattern, which often I find using myself:

f(x::Int; a = 0) = 1
f(x::Char) = 2
g(x; kw...) = f(x; kw...) + 1

Here hasmethod is not able to tell for which type of x one can use the keyword argument a.

If you have enough control over the situation, then you can use traits, for example as follows:

accepts_kw_a(::typeof(f), ::Type{Int}) = true
accepts_kw_a(::typeof(f), ::Type{Char}) = false
accepts_kw_a(::typeof(g), ::Type{T}) where T = accepts_kw_a(f, T)

Maybe you can insists that the person giving you a functions also write a method for the trait.

1 Like

You’d need the function and argument types to be constant too, then the compiler is free to do the call at compile-time and be invalidated when method definitions change. Obviously that’s much more limited than a runtime call.