Here is some code that studies performances of some implementations of effectively a lookup table.
#!/usr/bin/env julia
import BenchmarkTools
module Enums
@enum COLORS begin
RED
GREEN
BLUE
BROWN
end
global const MY_DICT::Dict{COLORS, Float64} = Dict(RED => 42.0, GREEN => 54.0, BLUE => 65.0, BROWN => 45.0)
function lookup_global(color::COLORS)::Float64
return MY_DICT[color]
end
function doit_global()::Nothing
for color::COLORS in instances(COLORS)
lookup_global(color)
end
return nothing
end
function lookup_local(color::COLORS)::Float64
my_dict::Dict{COLORS, Float64} = Dict(RED => 42.0, GREEN => 54.0, BLUE => 65.0, BROWN => 45.0)
return my_dict[color]
end
function doit_local()::Nothing
for color::COLORS in instances(COLORS)
lookup_local(color)
end
return nothing
end
end
# abstract types
module Abstract
abstract type AbstractColors end
struct RED <: AbstractColors end
struct GREEN <: AbstractColors end
struct BLUE <: AbstractColors end
struct BROWN <: AbstractColors end
lookup(::RED) = 42.0
lookup(::GREEN) = 54.0
lookup(::BLUE) = 65.0
lookup(::BROWN) = 45.0
global const COLORS_VEC = [ RED(), GREEN(), BLUE(), BROWN()]
function doit_global()::Nothing
for color::AbstractColors in COLORS_VEC
lookup(color)
end
return nothing
end
function doit_local()::Nothing
my_vec::Vector{AbstractColors} = [ RED(), GREEN(), BLUE(), BROWN()]
for color::AbstractColors in my_vec
lookup(color)
end
return nothing
end
end
println("\n\nEnums.doit_global()")
BenchmarkTools.@btime Enums.doit_global()
println("\n\nEnums.doit_local()")
BenchmarkTools.@btime Enums.doit_local()
println("\n\nAbstract.doit_global()")
BenchmarkTools.@btime Abstract.doit_global()
println("\n\nAbstract.doit_local()")
BenchmarkTools.@btime Abstract.doit_local()
Output:
Enums.doit_global()
24.299 ns (0 allocations: 0 bytes)
Enums.doit_local()
533.651 ns (16 allocations: 1.88 KiB)
Abstract.doit_global()
54.372 ns (0 allocations: 0 bytes)
Abstract.doit_local()
79.262 ns (1 allocation: 80 bytes)
I know that Julia can infer types. I know that global variables are not such a good idea and why. Never the less. Abstract.doit_local()
is a good representation of how in my company typically lookup tables are implemented. There is always some container with objects of children of an abstract type and there is dispatch. I know that containers with abstract types cause a performance penalty due to optimization issues with the compiler and memory.
An alternative could be to use an enum. It prevents the wild growth of functions and structs. To my surprise, Enums.doit_global()
is the fastest even though a const global variable is used. Developers appreciate Julia for its speed and if this way is fastest, people would be interested in an analysis on this - probably some low-level compiler optimization is going on.
Questions:
- What is the preferred way to implement a lookup table in Julia?
- What are experts opinions on using dispatch for a lookup table?
- Why is the
Enums.doit_global()
fastest?