Dynamic dispatch


#1

Hi, I’m confused with dynamic dispatch. Given this code :

f(x::Int32) = 32
f(x::Int64) = 64
g(x::Integer) = f(x)

@code_llvm g(0)
define i64 @julia_g_62846(i64) #0 !dbg !5 {
top:
  ret i64 64
}

I see that g(0) return directly 64 which means that g is statically dispatched (g is specialized for Int64 with inlined f).
But x in g(x::Integer) is declared as Integer which is an abstract type. In this case, I would expect a dynamic dispatch and consequently a more complex implementation of g.
When the dynamic dispatch occurs?


#2

When you call any function, a specialized version of that function is compiled given the argument types. This is true regardless of the declared argument types, and in fact is true even if you declared no types at all. For example, if you declared h(x) = f(x) it would do the same thing.

In this case, when you call g(0) you are passing an Int64 (0 on a 64-bit machine). The compiler says, "okay, x is an Int64, now specialize everything in g for that type of x. It looks inside g and finds a call to f(x) and thinks "aha, this is calling the f(x::Int64) method, which returns an Int so the return type of g(::Int64) is also Int64. Also, it notices that f(x::Int64) is really short, so it inlines it. The result is the one-line compiled code you saw.

If you subsequently call g(Int32(0)), then it will compile a second version of g, this one specialized for Int32 (so it will end up being hard-coded to return 32). These two compiled versions of g will stay in memory, and will be called whenever g is called with arguments of those types.

Dynamic (runtime) dispatch only occurs when the compiler can’t figure out the types. For example, if you do map(g, Any[1,2,Int32(3),4]), where it is applying g to an array of Any and therefore doesn’t know the type of x until runtime, it will compile a generic version of g that dispatches at runtime to the correct version of f.

This kind of aggressive type specialization and type inference is the key strength of Julia. It allows you to write really generic code that works for any type, and doesn’t declare any types, and yet which is at the same time fast (because it is specialized when the code calling it is compiled).


#3

You can actually see this happening in the LLVM — it is defining a function specifically for i64, not just any Integer. If you go on to ask for @code_llvm g(Int32(0)), you’ll see:

define i64 @julia_f_60910(i32) #0 !dbg !5 {
top:
  ret i64 32
}

Julia also tracks the fact that it statically performed this dispatch at compile-time, so it’s then able to go back and re-define the function (with new dispatch or inlined values) if any of its dependencies change.


#4

To (hopefully) add to the helpful posts above, it’s important to distinguish between dispatch and specialization. Dispatch is when Julia decides which method to call. In your example you have two different f methods, and when Julia hits a function call f(x) it will dispatch to the most-specific one that matches the type of x. In your case a call g(x) will dispatch to your method g(x::Integer) whenever x is any subtype of Integer. So dispatch is semantically-important to the language and affects how your code runs.

Specialization is the process where the first time a given method is called with a given set of argument types, the compiler usually will compile a specialized version of it. for instance, if you run g(Int32(0)) and g(Int64(0)), both calls will dispatch to the same method, but two different implementations will be compiled and run. So specialization is compiler optimization, and doesn’t affect the semantics of the language.


#5

Thanks, it’s very clear :slight_smile:


#6

I feel that the difference between static and dynamic dispatch and type specialization should be better described in the manual or made it easier to discover. Searching “julia dynamic dispatch” on Google gives as first results a few questions here on Discourse