# \[ANN\] Virtual.jl: optimizing dynamic calls away through "virtual" multiple dispatch

**URL:** <https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413>\
**Category:** Package Announcements\
**Tags:** announcement, dispatch, multidispatch\
**Created:** [July 18, 2022, 4:35pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413 "2022-07-18T16:35:21Z")\
**Posts on this page:** 15\
**Page:** 1

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 18, 2022, 4:35pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/1 "2022-07-18T16:35:21Z")

</div>

P.S: Recently I’m planning to implement a zero-latency scripting language in Julia, hence I need this feature. The package can be regarded as a generalization to [ValSplit.jl](https://github.com/ztangent/ValSplit.jl), supporting fast runtime polymorphisms for multiple arguments.

[This package (Virtual.jl)](https://github.com/thautwarm/Virtual.jl) provides two macros `@virtual` and `@override`, with which we can avoid those slow dynamic calls and produce orders of magnitude performance gain!

```julia
abstract type Animal end
struct Cat <: Animal end
struct Dog <: Animal end

@virtual f(x::Animal, y::Int) = y
@override f(x::Cat, y::Int) = 1 + y
@override f(x::Dog, y::Int) = 2 + y

julia> code_typed(f, (Animal, Int))
1-element Vector{Any}:
 CodeInfo(
1 ─ %1 = (x isa Cat)::Bool
└── goto #4 if not %1
2 ─ goto #4 if not true
3 ─ %4 = Base.add_int(1, y)::Int64
└── goto #8
4 ┄ %6 = (x isa Dog)::Bool
└── goto #7 if not %6
5 ─ goto #7 if not true
6 ─ %9 = Base.add_int(2, y)::Int64
└── goto #8
7 ┄ goto #8
8 ┄ %12 = φ (#3 => %4, #6 => %9, #7 => y)::Int64
└── return %12
) => Int64

```

The following example comes with a benchmark against hand-written if-else and dynamic multiple dispatch, which shows that the performance of Virtual.jl is best:

```julia
using Virtual, BenchmarkTools
abstract type Animal end
struct Dog <: Animal end
struct Tiger <: Animal end
struct Duck <: Animal end

@virtual fast_func(x::Animal, y::Int) = error("No default method for score!")
@override fast_func(x::Dog, y::Int) = 2 + y
@override fast_func(x::Tiger, y::Int) = 3 + y
@override fast_func(x::Duck, y::Int) = 4 + y

dyn_func(x::Animal, y::Int) = error("No default method for score!")
dyn_func(x::Dog, y::Int) = 2 + y
dyn_func(x::Tiger, y::Int) = 3 + y
dyn_func(x::Duck, y::Int) = 4 + y

manual_func(x::Animal, y::Int) =
    if x isa Dog
        2 + y
    elseif x isa Tiger
        3 + y
    elseif x isa Duck
        4 + y
    else
        error("No default method for score!")
    end

const samples = Animal[Dog(), Duck(), Tiger()]
animals = Animal[samples[rand(1:3)] for i = 1:100]

function sum_score(score_func, xs::AbstractVector{Animal})
    s = 0
    for x in xs
        s += score_func(x, 3)
    end
    return s
end

@info "fast_func via Virtual.jl"
display(@benchmark(sum_score(fast_func, animals)))
@info "manual split by hand"
display(@benchmark(sum_score(manual_func, animals)))
@info "dyn_func by dynamic multiple dispatch"
display(@benchmark(sum_score(dyn_func, animals)))

```

results:

```julia
[ Info: fast_func via Virtual.jl
BenchmarkTools.Trial: 10000 samples with 883 evaluations.
 Range (min … max): 118.913 ns … 982.673 ns ┊ GC (min … max): 0.00% … 84.44%
 Time (median): 139.751 ns ┊ GC (median): 0.00%
 Time (mean ± σ): 145.934 ns ± 28.422 ns ┊ GC (mean ± σ): 0.16% ± 1.46%

  ▄ ▄▃ ▂▁█▅▃▃▂▃
  █▇██▇████████▇▆▇▅▆▄▇▅▃▃▂▂▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▃
  119 ns Histogram: frequency by time 257 ns <

 Memory estimate: 16 bytes, allocs estimate: 1.
[ Info: manual split by hand
BenchmarkTools.Trial: 10000 samples with 818 evaluations.
 Range (min … max): 146.088 ns … 1.342 μs ┊ GC (min … max): 0.00% … 87.01%
 Time (median): 162.958 ns ┊ GC (median): 0.00%
 Time (mean ± σ): 168.780 ns ± 29.335 ns ┊ GC (mean ± σ): 0.14% ± 1.23%

  █ ▅▅▁▂ ▄
  █▅████▄█▃█▃█▃█▆▅▅▃▃▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
  146 ns Histogram: frequency by time 283 ns <

 Memory estimate: 16 bytes, allocs estimate: 1.
[ Info: dyn_func by dynamic multiple dispatch
BenchmarkTools.Trial: 10000 samples with 7 evaluations.
 Range (min … max): 4.043 μs … 19.000 μs ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 4.543 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 4.677 μs ± 624.903 ns ┊ GC (mean ± σ): 0.00% ± 0.00%

        █ ▆
  ▂▂▃▃▇▃█▆██▆▆▃▅▂▆▃▃▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
  4.04 μs Histogram: frequency by time 7.39 μs <

 Memory estimate: 256 bytes, allocs estimate: 16.

```

There are also limitations to this package, for instance, variadic parameters and keyword parameters are not supported. There is a complete list of the limitations: [Virtual.jl limitations](https://github.com/thautwarm/Virtual.jl#limitations).

Addressing such limitations is as hard as correctly re-implementing Julia’s type lattice, namely, too hard…

Enjoy! 🥳 🥳 🥳

---

<div class="post-metadata">

**Author:** ![adienes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/adienes/32/37459_2.png) [@adienes](https://discourse.julialang.org/u/adienes)\
**Post date:** [July 18, 2022, 7:12pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/2 "2022-07-18T19:12:46Z")

</div>

This may be a bit tangential to the package (and congrats on releasing it!) but it makes me think: why does this not happen by default in regular Julia, i.e. without the need for these macros? If dispatch on abstract containers can be made faster what are the reasons not to? Disclaimer I am still learning how Julia is built, so there may be something rather fundamental I am missing.

---

<div class="post-metadata">

**Author:** ![Christopher\_Fisher](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/christopher_fisher/32/26132_2.png) [@Christopher\_Fisher](https://discourse.julialang.org/u/Christopher_Fisher)\
**Post date:** [July 18, 2022, 8:55pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/3 "2022-07-18T20:55:46Z")

</div>

Thank you for putting this package together. I think it could be very useful.

I was wondering why the example no longer works when I change the type annotation from `Int` to `Integer`. Do the types need to be concrete?

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 12:18am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/4 "2022-07-19T00:18:07Z")

</div>

I tried changing all `Int` to `Integer`, it works.

Maybe you are defining overloaded methods whose signature is more general than the default method? See [limitations](https://github.com/thautwarm/Virtual.jl#limitations):

```julia
@virtual f(x::Number, y::Number) = 0

# wrong: Tuple{Float64, Any} <: Tuple{Number, Number} === false
@override f(x::Float64, y) = x * 3

# correct: Tuple{Float64, Number} <: Tuple{Number, Number} === true
@override f(x::Float64, y::Number) = x * 3

```

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 12:34am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/5 "2022-07-19T00:34:18Z")

</div>

I think one of the reason for not making Virtual.jl and ValSplit.jl a language builtin is, it causes too much recompilation. The dispatch method needs to be recompiled once a new method is defined.

This leads to a longer precompilation time.

Dynamic calls are considered slow, but in practice they are not slow at all if the logic inside the function is a heavy computation. Dynamic calls delay the compilation, and may trigger compilation if necessary.

---

<div class="post-metadata">

**Author:** ![Christopher\_Fisher](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/christopher_fisher/32/26132_2.png) [@Christopher\_Fisher](https://discourse.julialang.org/u/Christopher_Fisher)\
**Post date:** [July 19, 2022, 9:07am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/6 "2022-07-19T09:07:08Z")

</div>

Interesting. Maybe I am misunderstanding something. I was under the impression that I could change Int to Integer in your example. Here is the code I used on Julia 1.7.3:

> **Summary**
>
> ```julia
> using Virtual, BenchmarkTools
> abstract type Animal end
> struct Dog <: Animal end
> struct Tiger <: Animal end
> struct Duck <: Animal end
> 
> @virtual fast_func(x::Animal, y::Integer) = error("No default method for score!")
> @override fast_func(x::Dog, y::Integer) = 2 + y
> @override fast_func(x::Tiger, y::Integer) = 3 + y
> @override fast_func(x::Duck, y::Integer) = 4 + y
> 
> dyn_func(x::Animal, y::Integer) = error("No default method for score!")
> dyn_func(x::Dog, y::Integer) = 2 + y
> dyn_func(x::Tiger, y::Integer) = 3 + y
> dyn_func(x::Duck, y::Integer) = 4 + y
> 
> manual_func(x::Animal, y::Integer) =
> if x isa Dog
> 2 + y
> elseif x isa Tiger
> 3 + y
> elseif x isa Duck
> 4 + y
> else
> error("No default method for score!")
> end
> 
> const samples = Animal[Dog(), Duck(), Tiger()]
> animals = Animal[samples[rand(1:3)] for i = 1:100]
> 
> function sum_score(score_func, xs::AbstractVector{Animal})
> s = 0
> for x in xs
> s += score_func(x, 3)
> end
> return s
> end
> 
> @info "fast_func via Virtual.jl"
> display(@benchmark(sum_score(fast_func, animals)))
> @info "manual split by hand"
> display(@benchmark(sum_score(manual_func, animals)))
> @info "dyn_func by dynamic multiple dispatch"
> display(@benchmark(sum_score(dyn_func, animals)))
> 
> ```

Here are the results:

[ Info: fast\_func via Virtual.jl

```julia
BenchmarkTools.Trial: 10000 samples with 8 evaluations.
 Range (min … max): 3.762 μs … 16.781 μs ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 3.826 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 3.960 μs ± 521.806 ns ┊ GC (mean ± σ): 0.00% ± 0.00%

  ██▁█▅▃▂ ▂
  ████████▇▇▆▆▆▆▇▇▇▆▅▅▆▅▄▄▇▆▆▇▇▆▇▇▆▆▆▄▄▆▅▅▅▁▃▁▄▃▄▃▄▄▅▆▆▅▆▅▆▆▆ █
  3.76 μs Histogram: log(frequency) by time 6.56 μs <

 Memory estimate: 256 bytes, allocs estimate: 16.
[ Info: manual split by hand
BenchmarkTools.Trial: 10000 samples with 893 evaluations.
 Range (min … max): 126.036 ns … 1.944 μs ┊ GC (min … max): 0.00% … 90.37%
 Time (median): 126.924 ns ┊ GC (median): 0.00%
 Time (mean ± σ): 134.716 ns ± 32.419 ns ┊ GC (mean ± σ): 0.34% ± 1.57%

  █ ▅▅▁ ▁▁ ▁
  █████████████▇██▇██▇▇▇▇▇▇▇█▇▇▇▇▇▇▆▆▇▆▆▆▆▆▆▆▆▆▅▆▆▅▅▅▆▅▅▅▅▄▆▅▅ █
  126 ns Histogram: log(frequency) by time 216 ns <

 Memory estimate: 16 bytes, allocs estimate: 1.
[ Info: dyn_func by dynamic multiple dispatch
BenchmarkTools.Trial: 10000 samples with 8 evaluations.
 Range (min … max): 3.008 μs … 9.808 μs ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 3.174 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 3.346 μs ± 611.198 ns ┊ GC (mean ± σ): 0.00% ± 0.00%

  ▆█▅▃▇▅▃▃▁ ▁▁ ▁▁ ▂
  █████████▇▇▆▅▅▆▅▇▇▇▅▆▅▅▅▅▆▇▆▆▇▇▇▇█▇███████████▇▆▅▄▅▅▃▅▆▆▅▆▆ █
  3.01 μs Histogram: log(frequency) by time 5.67 μs <

 Memory estimate: 256 bytes, allocs estimate: 16.

```

---

<div class="post-metadata">

**Author:** ![greatpet](https://avatars.discourse-cdn.com/v4/letter/g/e495f1/32.png) [@greatpet](https://discourse.julialang.org/u/greatpet)\
**Post date:** [July 19, 2022, 9:50am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/7 "2022-07-19T09:50:01Z")

</div>

Why is it faster than manual splitting? The output of `code_typed` looks very similar to the manual version.

---

<div class="post-metadata">

**Author:** ![ericphanson](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ericphanson/32/215186_2.png) [@ericphanson](https://discourse.julialang.org/u/ericphanson)\
**Post date:** [July 19, 2022, 10:09am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/8 "2022-07-19T10:09:50Z")

</div>

Could you say something about how the package was made? I’ve never seen Julia code like that before!

Is the [deserialization bit](https://github.com/thautwarm/Virtual.jl/blob/main/src/reflection.compiled.jl) for latency reasons? That technique kind of scares me because I’m not sure how to review the code being loaded.

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 10:41am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/9 "2022-07-19T10:41:59Z")

</div>

This is because we will always the signature from `@virtual` to box values (to assure the generared dispatch funxction is statically called), while manual split can do specialization in the callsite. If you provide the second argument of `score_func` using an element from `Vector{Number}`, the three funcs will be the same slow.

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 10:45am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/10 "2022-07-19T10:45:27Z")

</div>

Use `Int` instead of `Integer` for `@override` methods will produce fast code, while keeping `@virtual f`’s parameters abstract is okay.

Besides, if `+` is implemented with Virtual.jl, our `fast_func` can be still fast.

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 10:50am UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/11 "2022-07-19T10:50:30Z")

</div>

Just check `reflection.jl`, it is the source code.

For serialization stuffs, I’m recently experimenting with “dev-only dependencies” in Julia, because a library made by me, MLStyle, is runtime-free (usually not used at all after precompilation).

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 3:36pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/12 "2022-07-19T15:36:10Z")

</div>

I’d like to expand a little.

Your code using `Integer`:

```julia
@virtual fast_func(x::Animal, y::Integer) = error("No default method for score!")
@override fast_func(x::Dog, y::Integer) = 2 + y
@override fast_func(x::Tiger, y::Integer) = 3 + y
@override fast_func(x::Duck, y::Integer) = 4 + y

```

This will work like

```julia
# struct Virtual.GenWrapper
# value
# end
function fast_func(_x::Virtual.GenWrapper, _y::Virtual.GenWrapper)
   x = _x.value
   y = _y.value
    if x isa Dog && y isa Integer
        2 + y
    elseif x isa Tiger && y isa Integer
        3 + y
    elseif x isa Duck && y isa Integer
        4 + y
    else
        error("No default method for score!")
    end
end

```

As can be seen, (EDIT) `@override`’s type annotations affect the optimization of using `isa` to narrow argument types.

Modifying your code to the following one produce fast code as well:

```julia
@virtual fast_func(x::Animal, y::Integer) = error("No default method for score!")
@override fast_func(x::Dog, y::Int) = 2 + y
@override fast_func(x::Tiger, y::Int) = 3 + y
@override fast_func(x::Duck, y::Int) = 4 + y

# the generated fast_func is

function fast_func(_x::Virtual.GenWrapper, _y::Virtual.GenWrapper)
   x = _x.value
   y = _y.value
    if x isa Dog && y isa Int
        2 + y
    elseif x isa Tiger && y isa Int
        3 + y
    elseif x isa Duck && y isa Int
        4 + y
    else
        error("No default method for score!")
    end
end

```

Now maybe you see why there is a performance gap?

---

<div class="post-metadata">

**Author:** ![Christopher\_Fisher](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/christopher_fisher/32/26132_2.png) [@Christopher\_Fisher](https://discourse.julialang.org/u/Christopher_Fisher)\
**Post date:** [July 19, 2022, 7:13pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/13 "2022-07-19T19:13:07Z")

</div>

Thanks for your explanation. I am still a bit confused about the cases in which Virtual.jl will work. Based on the documentation, I thought the following would work because` Integer <: Number`. However, it did not seem to work.

```julia
@virtual fast_func(x::Animal, y::Number) = error("No default method for score!")
@override fast_func(x::Dog, y::Integer) = 2 + y
@override fast_func(x::Tiger, y::Integer) = 3 + y
@override fast_func(x::Duck, y::Integer) = 4 + y

```

---

<div class="post-metadata">

**Author:** ![iamed2](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/iamed2/32/215082_2.png) [@iamed2](https://discourse.julialang.org/u/iamed2)\
**Post date:** [July 19, 2022, 8:23pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/14 "2022-07-19T20:23:42Z")

</div>

Some more discussion on this happened here: [Union splitting vs C++ - #14 by lmiq](https://discourse.julialang.org/t/union-splitting-vs-c/61772/14)

---

<div class="post-metadata">

**Author:** ![thautwarm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thautwarm/32/37760_2.png) [@thautwarm](https://discourse.julialang.org/u/thautwarm)\
**Post date:** [July 19, 2022, 10:43pm UTC](https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413/15 "2022-07-19T22:43:56Z")

</div>

It works for me, although the generated code for this case is slow:

(EDIT: make the code copy-paste runnable)

```julia
using Virtual, BenchmarkTools
abstract type Animal end
struct Dog <: Animal end
struct Tiger <: Animal end
struct Duck <: Animal end

@virtual fast_func(x::Animal, y::Number) = error("No default method for score!")
@override fast_func(x::Dog, y::Integer) = 2 + y
@override fast_func(x::Tiger, y::Integer) = 3 + y
@override fast_func(x::Duck, y::Integer) = 4 + y

fast_func(Dog(), 1)
# 3

fast_func(Tiger(), 2)
# 5

fast_func(Duck(), 3)
# 7

test_results(animals::Vector{Animal}) =
           [fast_func(x, 0) for x in animals]
# test_results (generic function with 1 method)

test_results([Dog(), Tiger(), Duck()])
# 3-element Vector{Int64}:
# 2
# 3
# 4

```

> **code\_typed result:**
>
> ```julia
> julia> code_typed(fast_func, (Any, Any))
> 1-element Vector{Any}:
> CodeInfo(
> 1 ── %1 = Virtual.GenWrapper::Type{Virtual.GenWrapper}
> │ %2 = %new(%1, x)::Virtual.GenWrapper
> │ %3 = Virtual.GenWrapper::Type{Virtual.GenWrapper}
> │ %4 = %new(%3, y)::Virtual.GenWrapper
> │ %5 = Main.fast_func::typeof(fast_func)
> │ %6 = Core.tuple(%5, %2, %4)::Tuple{typeof(fast_func), Virtual.GenWrapper, Virtual.GenWrapper}
> │ %7 = (x isa Tiger)::Bool
> └─── goto #4 if not %7
> 2 ── %9 = (y isa Integer)::Bool
> └─── goto #4 if not %9
> 3 ── %11 = π (x, Tiger)
> │ %12 = π (y, Integer)
> │ %13 = invoke var"##368"(%11::Tiger, %12::Integer)::Any
> └─── goto #14
> 4 ┄─ %15 = (x isa Duck)::Bool
> └─── goto #7 if not %15
> 5 ── %17 = (y isa Integer)::Bool
> └─── goto #7 if not %17
> 6 ── %19 = π (x, Duck)
> │ %20 = π (y, Integer)
> │ %21 = invoke var"##369"(%19::Duck, %20::Integer)::Any
> └─── goto #14
> 7 ┄─ %23 = (x isa Dog)::Bool
> └─── goto #10 if not %23
> 8 ── %25 = (y isa Integer)::Bool
> └─── goto #10 if not %25
> 9 ── %27 = π (x, Dog)
> │ %28 = π (y, Integer)
> │ %29 = invoke var"##367"(%27::Dog, %28::Integer)::Any
> └─── goto #14
> 10 ┄ %31 = (x isa Animal)::Bool
> └─── goto #13 if not %31
> 11 ─ %33 = (y isa Number)::Bool
> └─── goto #13 if not %33
> 12 ─ (var"##366")(x, y)::Union{}
> └─── unreachable
> 13 ┄ %37 = invoke Base.print_to_string(%6::Tuple{typeof(fast_func), Virtual.GenWrapper, Virtual.GenWrapper})::String
> │ %38 = invoke Base.string("method not found for "::String, %37::String)::String
> │ invoke error(%38::String)::Union{}
> └─── unreachable
> 14 ┄ %41 = φ (#3 => %13, #6 => %21, #9 => %29)::Any
> └─── return %41
> ) => Any
> 
> ```
