I have written small tree type for another purpose but am using it here in order to test evaluation speed and these are the results:
# Small Prototype for a new tree
import Base.show
using SymEngine
abstract type Node end
abstract type OpNode <: Node end
mutable struct VarNode <: Node
symbol::Symbol
val::Float64
end
show(io::IO, a::VarNode) = print(io, "VarNode $(a.symbol)")
mutable struct ConstNode <: Node
val::Float64
end
show(io::IO, a::ConstNode) = print(io, "ConstNode $(a.val)")
mutable struct AffOpNode <: OpNode
# like +, -, *
symbol::Symbol # operator
val::Float64
children::Array{Node} # ASSUME only 1 or 2 children
end
show(io::IO, a::OpNode) = print(io, "OpNode $(a.symbol) \n $(a.children)")
mutable struct PWLOpNode <: OpNode
symbol::Symbol # operator like max or min or relu
val::Float64
children::Array{Node}
end
"""
A function to replace eval. Hopefully faster.
"""
function apply_fun(op, args)
if op == :+
return +(args...)
elseif op == :-
return -(args...)
elseif op == :*
return *(args...)
elseif op == :max
return max(args...)
elseif op == :min
return min(args...)
elseif op == :relu
return relu(args...)
end
end
"""
A function to turn an Expr into a Computational Graph with Nodes.
"""
function populate_graph(e::Expr)
@assert e.head == :call
# args
wrapped_args = [populate_graph(a) for a in e.args[2:end]]
# return parent node
if e.args[1] ∈ [:+, :-, :*, :/]
return AffOpNode(e.args[1], "", 0.0, wrapped_args)
elseif e.args[1] ∈ [:max, :min]
return PWLOpNode(e.args[1], "", [], 0.0, wrapped_args)
else
warn("Whoa bro wassup here...")
end
end
function populate_graph(s::Symbol)
return VarNode(s, "", 0.0)
end
function populate_graph(r::T where T <: Real)
return ConstNode(r)
end
"""
A function to evaluate a sample.
"""
function eval_sample(n::OpNode, sample; mode="custom")
# evaluate arguments
args = [eval_sample(a, sample) for a in n.children]
if mode == "custom"
return apply_fun(n.symbol, args)
elseif mode == "eval"
return eval(Expr(:call, n.symbol, args...))
elseif mode == "symengine"
return Basic(Expr(:call, n.symbol, args...))
elseif mode == "getproperty"
return getproperty(Base, n.symbol)(args...)
end
end
eval_sample(n::VarNode, sample) = sample[n.symbol]
eval_sample(n::ConstNode, sample) = n.val
# testing ~~~~~~~~~~~~~~~
# begin with Expr and population this type
e = :(max(min(x,y),2.45))
g = populate_graph(e)
# test eval
x_s, y_s = rand(), rand()
## first with normal functions
t1 = @elapsed max(min(x_s, y_s), 2.45)
## then evaluating the graph with case switch
t2 = @elapsed eval_sample(g, Dict(:x => x_s, :y => y_s); mode="custom")
## Compared to running eval in graph
t2p5 = @elapsed eval_sample(g, Dict(:x => x_s, :y => y_s); mode="eval")
## compare to running eval natively
t3 = @elapsed eval(:(max(eval(:(min(x_s,y_s))),2.45)))
# compare to running SymEngine
t4 = @elapsed eval_sample(g, Dict(:x => x_s, :y => y_s); mode="symengine")
# compare to getproperty
t5 =@elapsed eval_sample(g, Dict(:x => x_s, :y => y_s); mode="getproperty")
println("t1 native = $t1, \n t2 custom = $t2, \n t2p5 eval in graph = $t2p5, \n t3 eval = $t3, \n t4 symengine = $t4, \n t5 getproperty = $t5")
one run:
t1 native = 1.1577e-5,
t2 custom = 3.3504e-5,
t2p5 eval in graph = 0.000177618,
t3 eval = 0.000284704,
t4 symengine = 9.8378e-5
Another run:
t1 native = 2.7059e-5,
t2 custom = 7.0033e-5,
t2p5 eval in graph = 0.000185254,
t3 eval = 0.000314434,
t4 symengine = 0.000200453,
t5 getproperty = 3.063e-5
In short the speed rankings are:
1 - native
2 - getproperty
3 - my custom lookup (works for a limited subset of functions)
4 - symengine and eval in the graph are about tied
5 - eval written out explicitly
However I have to ask @ffevotte why it takes only nanoseconds for you to run +,- etc. but microseconds for me to run max and min…do you know?
Edit: Tried a simpler expression with just +,- and nothing seems to be running faster than microseconds for me…maybe you just have a fast computer?