Hello everyone,
Sorry, it’s going to be a bit long.
I’m trying to reach performance in Julia for function calls that depends on a variable. It means given a line in my code, I can’t tell which function will be run in advance (I only know its type signature).
So I’ve created a struct type to store the function within it. However, I’m facing performance issues. I’ve made some benchmark in order to investigate.
import Statistics: mean
using BenchmarkTools
# Creation of types that store a Function or a function Symbol.
EdgeTransition = Union{Nothing,Vector{Symbol}}
struct EdgeStruct
tr::EdgeTransition
func1::Function
func2::Function
end
struct EdgeStruct2
tr::EdgeTransition
func1::Symbol
func2::Symbol
end
EdgeTuple = Tuple{EdgeTransition,Function,Function}
EdgeTuple2 = Tuple{EdgeTransition,Symbol,Symbol}
# Function that will be called through an edge object
function f(t::Float64, values::Vector{Float64}, x::Vector{Int}, p::Vector{Float64})
return (t <= 0.025 && (values[1] < 50.0 || values[1] > 75.0))
end
t = 0.1
values = [100.0, Inf, 0.0]
x = [99, 99, 1, 0]
p = [1.0, 1.0]
edge_struct_1 = EdgeStruct(nothing, getfield(Main, :f), getfield(Main, :mean))
edge_struct_2 = EdgeStruct2(nothing, :f, :mean)
edge_tuple_1 = (nothing, getfield(Main, :f), getfield(Main, :mean))
edge_tuple_2 = (nothing, :f, :mean)
@assert typeof(edge_struct_1) <: EdgeStruct && typeof(edge_struct_2) <: EdgeStruct2 &&
typeof(edge_tuple_1) <: EdgeTuple && typeof(edge_tuple_2) <: EdgeTuple2
println("Time execution of f")
@btime f(t, values, x, p)
println("Time execution of f with edges")
println("- Structs")
@btime getfield(edge_struct_1, :func1)(t, values, x, p)
@btime getfield(Main, getfield(edge_struct_2, :func1))(t, values, x, p)
println("- Tuples")
@btime edge_tuple_1[2](t, values, x, p)
@btime getfield(Main, edge_tuple_2[2])(t, values, x, p)
println("Time access of variables")
println("- Structs")
@btime getfield(edge_struct_1, :func1)
@btime getfield(edge_struct_2, :func1)
println("- Tuples")
@btime edge_tuple_1[2]
@btime edge_tuple_2[2]
which gives the output:
Time execution of f
4.466 ns (0 allocations: 0 bytes)
Time execution of f with edges
- Structs
24.571 ns (0 allocations: 0 bytes)
46.807 ns (0 allocations: 0 bytes)
- Tuples
262.293 ns (0 allocations: 0 bytes)
275.926 ns (0 allocations: 0 bytes)
Time access of variables
- Structs
10.826 ns (0 allocations: 0 bytes)
10.826 ns (0 allocations: 0 bytes)
- Tuples
235.832 ns (0 allocations: 0 bytes)
237.547 ns (0 allocations: 0 bytes)
I’ve considered four ways of storing a function label in a variable. The best I’ve found so far, according to the BenchmarkTools package, is to create a new struct type, stores the function in a field of type func1::Function
and get access to the function via getfield(Main, :func1)
.
However, one can see the time execution of f()
plus the time memory access of the edge_struct_1
variable is about 15ns, but the call of getfield(edge_struct_1, :func1)(t, values, x, p)
is about 25ns.
Where are the 10ns left? I’ve pursued my investigations and found via the --track-allocation=user
option and Coverage.jl
package that an allocation of memory is made each time getfield(edge_struct_1, :func1)(t, values, x, p)
is executed in my code. This refutes the result of @btime
.
10ns and one allocation (actually 2 allocations because two functions are called the same way) is not that much but this function is executed a huge number of times and I think it has a quite big impact on the performance of my methods, especially the memory allocation.
Did I miss something or made an error about my conclusions? Does someone have any idea to avoid this allocation by implementing this problem from another point of view?
Thank you for your attention!