Julia: Mandatory keyword arguments and dynamic dispatch / performance

Created on 26 Jan 2020  Â·  6Comments  Â·  Source: JuliaLang/julia

Keyword arguments with types that depend on their value currently trigger dynamic dispatch. From discussing this with @MasonProtter on Slack, the reason is that optional keyword arguments such as f(;a=1,b=2,c=3,d=4,e=5) trigger a combinatorial explosion in possible methods.

I suggest for consideration that mandatory keyword arguments should participate in dispatch, because they are mandatory and don't contribute to the above issue. My use case for this is specifying output types via keyword arguments explicitly, resulting in code that expresses its intent in a manner easy for developers to see. There very well may be other use cases.

MWE below.

struct SomeStruct{T<:Integer}
end 

function outer_function_slow(input::SomeStruct{T}) where T
  inner_function_slow(input; output_type = T)
end

function inner_function_slow(x; output_type::Type{T}) where T<:Integer
  zero(T) # dynamic dispatch
end

function outer_function_fast(input::SomeStruct{T}) where T
  inner_function_fast(input, T)
end

function inner_function_fast(x, output_type::Type{T}) where T<:Integer
  zero(T) # no dynamic dispatch
end
julia> s = SomeStruct{Int32}()
SomeStruct{Int32}()

julia> @code_warntype outer_function_fast(s)
Variables
  #self#::Core.Compiler.Const(outer_function_fast, false)
  input::Core.Compiler.Const(SomeStruct{Int32}(), false)

Body::Int32
1 ─ %1 = Main.inner_function_fast(input, $(Expr(:static_parameter, 1)))::Core.Compiler.Const(0, false)
└──      return %1

julia> @code_warntype outer_function_slow(s)
Variables
  #self#::Core.Compiler.Const(outer_function_slow, false)
  input::Core.Compiler.Const(SomeStruct{Int32}(), false)

Body::Any
1 ─ %1 = (:output_type,)::Core.Compiler.Const((:output_type,), false)
│   %2 = Core.apply_type(Core.NamedTuple, %1)::Core.Compiler.Const(NamedTuple{(:output_type,),T} where T<:Tuple, false)
│   %3 = Core.tuple($(Expr(:static_parameter, 1)))::Core.Compiler.Const((Int32,), false)
│   %4 = (%2)(%3)::NamedTuple{(:output_type,),Tuple{DataType}}
│   %5 = Core.kwfunc(Main.inner_function_slow)::Core.Compiler.Const(var"#kw##inner_function_slow"(), false)
│   %6 = (%5)(%4, Main.inner_function_slow, input)::Any
└──      return %6

Most helpful comment

IICU, one might want to turn:

julia> f(T=Int)
NamedTuple{(:T,),Tuple{DataType}}

into

julia> f(T=Int)
NamedTuple{(:T,),Tuple{Type{Int}}}

The former lost information, whereas the latter preserves it.

All 6 comments

Personally, I’d love being able to dispatch on mandatory kwargs.

To play devils advocate, my main concern would be that the fact that kwargs don’t participate in dispatch is already a subtle and unintuitive source of bugs. There’s an argument to be made that this would be even more subtle.

All I did was give a keyword argument a default value and now my code won’t work! What gives?

Also, I think there is a misconception here:

Keyword arguments currently always trigger dynamic dispatch.

Keyword arguments should only be causing dynamic dispatches in cases where types depend on the value of the keyword argument. The performance problems you’re seeing are pretty niche.

Also, I think there is a misconception here:

Keyword arguments currently always trigger dynamic dispatch.

Keyword arguments should only be causing dynamic dispatches in cases where types depend on the value of the keyword argument. The performance problems you’re seeing _are_ pretty niche.

Thanks for the catch - didn't think quite enough about what my sentence actually meant while writing it. Edited the original issue to clarify. 🙂

Personally, I’d love being able to dispatch on mandatory kwargs.

To play devils advocate, my main concern would be that the fact that kwargs don’t participate in dispatch is already a subtle and unintuitive source of bugs. There’s an argument to be made that this would be even more subtle.

All I did was give a keyword argument a default value and now my code won’t work! What gives?

Yes, but on the flip side, the situations where one might encounter such issues also become more rare.

On another note, it would also be good to document this in the performance tips page (https://docs.julialang.org/en/v1/manual/performance-tips/) so that some poor fellow like me might actually consider this scenario as a reason for unexpectedly slow code.

The performance issue here is separate from the dispatch behavior. It's because we don't specialize the keyword argument container on every type:

julia> f(;kw...) = typeof(kw.data);

julia> f(T=Int)
NamedTuple{(:T,),Tuple{DataType}}

But that could be changed without changing how keyword arguments dispatch.

Sorry Jeff, could you clarify what that example you just gave shows? I'm not sure I see how that demonstrates the lack of specialization.

IICU, one might want to turn:

julia> f(T=Int)
NamedTuple{(:T,),Tuple{DataType}}

into

julia> f(T=Int)
NamedTuple{(:T,),Tuple{Type{Int}}}

The former lost information, whereas the latter preserves it.

Was this page helpful?
0 / 5 - 0 ratings