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, supporting fast runtime polymorphisms for multiple arguments.
This package (Virtual.jl) provides two macros @virtual
and @override
, with which we can avoid those slow dynamic calls and produce orders of magnitude performance gain!
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:
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:
[ 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.
Addressing such limitations is as hard as correctly re-implementing Juliaβs type lattice, namely, too hardβ¦
Enjoy!