Luke Adams said:
Perhaps a stupid question, but how is that different than what Julia itself does during the dynamic dispatch?
That's the right question to ask. Here's an analogy that helps only if you remember the old days when we looked up phone numbers in a book. Dynamic dispatch is like "here's the phone book, look up the number by the name of the person you're trying to call" and you start flipping through the "S" section. Union-splitting is like having someone print out a list of the 5 people you call most frequently and post it right next to your phone.
To be a little more precise & technical: when Julia compares concrete types, it's literally a pointer comparison: does 0x00007fa0d702c0c0 equal 0x00007fa0d74d7770? That's just a CPU cycle or so. In contrast, if Julia has to figure out what method to call, and one of your methods is written for arguments (a::AbstractVector{T} where T<:Real, b::AbstractString), then the process of deciding whether the types fit is vastly more complicated: you have to engage the whole subtyping machinery. That's very well optimized machinery, but there's no way even in principle to make it as cheap as a single pointer comparison. Moreover, if you don't know in advance which method will match, there's a chance you'll have to do this comparison for many method candidates, in the worst case the entire list of methods.
When it's complicated, then there's only one way to save the time it takes to do the lookup: to do it when the method is compiled. That happens automatically for you when Julia can infer concrete types, but in cases like the one we're discussing it's not possible even in principle to do so. This manual union-splitting solves the problem in one or two ways. First, if your isa comparisons are precise enough to determine a unique method, Julia can save the runtime cost of flipping through the pages of the phone book and do that lookup at compile time---that's essentially like printing out your "most frequently called numbers" list. Second, if your isa comparisons are against a concrete type (not an abstract type, not a Union), then Julia will reduce those type comparisons to a single pointer check---that's like making it so you can recognize which of your favorite numbers you need to call with a glance, and not have to fire up something analogous to regex machinery to match the name of the person. You get real benefit from each of these, and when both apply it's really fast.