Hi Julia community,
I am trying to build a tree-equations structure, which should evaluate as efficiently as possible. I’ve tried many different approaches so far, but can’t get rid of some allocations and some of the behavior is confusing me. I am open to any suggestions to speed this up.
To build equations, I have an abstract supertype and three types of mutable structs. The opera_node is for *, +, - (arity==2) and cos, sin, etc (arity == 1).
The const_leav is just a constant and the varia_leav holds the index to the variable, which is in a tuple of tuples.
abstract type equation_nodes end
mutable struct opera_node <: equation_nodes
arity::Int64; opera::Int64
lef::equation_nodes; rig::equation_nodes
opera_node(a::Int64, o::Int64) = (x = new(); x.arity = a; x.opera = o; x)
opera_node(a::Int64, o::Int64, l::equation_nodes) = (x = new(); x.arity = a; x.opera = o; x.lef = l; x)
opera_node(a::Int64, o::Int64, l::equation_nodes, r::equation_nodes) = (x = new(); x.arity = a; x.opera = o; x.lef = l; x.rig = r; x)
end
mutable struct const_leav <: equation_nodes
value::Float64
end
mutable struct varia_leav <: equation_nodes
value::Int64
end
vars = ((1.0, 2.0, 3.4, 4.1, 5.3, 6.3), (7.0, 2.0, 2.4, 1.1, 5.0, 6.4))
The function which evaluates the equation goes down recursively and evaluate from the bottom up:
function eval_equation(eq::equation_nodes, vars::Tuple, ops::Tuple)::typeof(vars[1])
if typeof(eq) <: const_leav
zeros = vars[1] .- vars[1] # to get a 0.0-tuple of appropriate length
cur_val = zeros .+ eq.value # and ensure to return always same type
elseif typeof(eq) <: varia_leav
cur_val = vars[eq.value]
elseif eq.arity == 1
cur_val = ops[eq.opera].(eval_equation(eq.lef, vars, ops))
# cur_val = ops[1][eq.opera].(eval_equation(eq.lef, vars, ops))
elseif eq.arity == 2
cur_val = ops[eq.opera].(eval_equation(eq.lef, vars, ops), eval_equation(eq.rig, vars, ops))
# cur_val = ops[2][eq.opera].(eval_equation(eq.lef, vars, ops), eval_equation(eq.rig, vars, ops))
end
return cur_val
end
Now, if I test the same simple equation, that essentially just adds two tuples, I get a longer execution time and different results depending on how many entries there are in the ops tuple containing the operations, although they are unused.
eq1 = opera_node(2, 1, varia_leav(1), varia_leav(2))
# comparison
f(vars) = vars[1] .+ vars[2]
@btime f($vars)
# --> 0.875 ns (0 allocations: 0 bytes)
# with three entries in ops
ops = (+, *, cos) # -> 50 ns with 3 entries and 200 ns with 4+ entries
@btime eval_equation($eq1, $vars, $ops)
# --> 48.155 ns (4 allocations: 352 bytes)
# with unused 4th entry -> huh?
ops = (+, *, cos, -) # -> 50 ns with 3 entries and 200 ns with 4+ entries
@btime eval_equation($eq1, $vars, $ops)
# --> 268.736 ns (7 allocations: 640 bytes)
In the end, I’d like to have a tuple of tuples like the following, to have a better distinction of the operation, but this yields even longer calculation times and more allocations:
ops = ((sin, cos), (+, -, *, /)) # -> supposed to be in the end
eq2 = opera_node(2, 1, const_leav(2), opera_node(2, 3, const_leav(5), opera_node(1, 2, opera_node(2, 3, const_leav(5), varia_leav(1)))))
# the out-commented lines in the elseifs of the eval_equation need to be switched, because of the nesting of the ops - tuple
# --> 891.667 ns (24 allocations: 2.11 KiB)
Do I make someting wrong with the typing and is there something I am missing? Maybe I need to add someting to the AbstractType? How can the results be this different? I love Julia and I want to get the best out of it
Thank you very much in advance