Diagnosing Multiple Dispatch and Type Stability in Julia
Julia's multiple dispatch can fall back to generic methods, hurting performance. This post shows how @which reveals the called method and how type annotations unlock native-speed specialization.
22 Mar 2026, 23:07 UTC

How Julia picks a method at runtime
Julia's multiple dispatch evaluates the exact types of every argument at call time. Unlike single-dispatch languages that look only at the receiver Julia inspects all arguments walks the method table and selects the best-matching signature. When a unique match exists the call proceeds directly. If two or more methods tie Julia raises a MethodError for ambiguity. If no method matches it may trigger implicit conversion or a fallback which often costs performance.
The @which diagnostic
The @which macro prints the method that Julia will invoke for a given call without executing the function. It is the fastest way to verify dispatch behavior:
@which myfunc(arg1 arg2)If the output lists a method with your concrete argument types dispatch is explicit. A generic indication means the compiler could not specialize and the runtime will use a fallback path.
A worked example from generic to type-stable
Start with an untyped dot-product:
function dot(u v) s = 0.0 for i in eachindex(u) s += u[i] * v[i] end return s endRunning @which dot([1.0 2.0] [3.0 4.0]) returns a method involving AbstractArray and Float64 signaling generic iteration.
Now annotate the arguments to concrete types:
function dot(x::Vector{Float64}, y::Vector{Float64}) s = 0.0 for i in eachindex(x) s += x[i] * y[i] end return s end@which dot([1.0 2.0] [3.0 4.0]) now reports the method specialized for Vector{Float64}. Pair this with @time:
@time dot(rand(10_000) rand(10_000))On the first call Julia compiles the specialized method; subsequent runs execute at native speed often matching or exceeding a C implementation for this kernel. In Julia 1.10+ this behavior is consistent across point releases.
Checking the generated dispatch
- Method match: @which returns a method whose parameter types align with your annotations.
- Compilation cost: @time on repeated calls should show a one-time compile stall then steady execution.
- Method count: methods(dot) should display a single concrete method rather than a family of fallbacks.
If dispatch remains generic verify that you are passing concrete types at the call site Julia type inference cannot cross dynamic boundaries.
Trade-offs and limitations
Excessive type specialization can increase compile time and code size. In large packages annotating every argument with concrete types may cause method explosion where the compiler generates many variants and the first call to each unique type combination incurs a noticeable latency penalty.
Method ambiguity is another practical limit. When two signatures match a call equally well Julia raises a MethodError or attempts implicit conversion which may silently degrade performance. Use @which early to surface conflicts between closely related signatures.
Practical verification checklist
- Define your function with the most specific types needed for the performance-critical path.
- Run @which with representative arguments and confirm the reported method matches your intent.
- Run @time and observe whether the first-call compilation cost is isolated and subsequent runs are fast.
- If you see multiple methods run methods(fun) and prune overly broad signatures.
- On a fresh Julia install (1.10+) repeat the checks stable dispatch should persist across point releases.
Actionable closing
If your Julia code feels slower than expected start with @which. Add the minimal type annotations that describe the argument shapes you actually use re-check @which and measure with @time. You'll often find that a two-line change turns a generic loop into a specialized native kernel all without leaving the Julia ecosystem.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.