Metaprogramming with @generated

I would like to write a simple function where I give it the operator I would like to apply as a symbol and it returns a runnable function that I can actually use. e.g.

@generated genmyfun( op , args) = :($op ( args ))
And I would use it like:
genmyfun( :+, args)([5,6]) and it would return 11.

Essentially, I want to recreate the behavior of eval, but in a way that is faster. Any ideas?

You can’t, I think. Why does the operator have to be a symbol? If you want to make a version of a function that can be called on an iterable of arguments, you can use Base.splat or Splat in recent Julia versions:

julia> Base.splat(+)([5, 6])
11
3 Likes

I’m not sure @generated is a good tool here.

When eval is only used to evaluate a variable (identified by its name as a Symbol) and retrieve the value bound to it, it’s sometimes possible to use getproperty(module, name) instead:

julia> args = (5, 6);
julia> op = :+;

julia> getproperty(Base, op)(args...)
11

I’m not sure how you plan to use this, but a simple benchmark against a plain eval could look like this:

function f1(args)
    res = 0.0
    for op in (:+, :-, :*, :/)
        res += getproperty(Base, op)(args...)
    end
    res
end

function f2(args)
    res = 0.0
    for op in (:+, :-, :*, :/)
        res += @eval $op(args...)
    end
    res
end
julia> using BenchmarkTools
julia> args = (5, 6);

julia> @btime f1($args)
  143.053 ns (5 allocations: 80 bytes)
40.833333333333336

julia> @btime f2($args)
  193.963 μs (161 allocations: 7.77 KiB)
40.833333333333336
3 Likes

I know of splat, it’s not what I’m looking for, but thanks anyway.

thanks @ffevotte this is great! I did not know of get_property. It is amazingly fast compared to eval!

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?

I think you’re referring to the difference between @btime and @elapsed?

julia> @elapsed f1(args)
1.83e-5

julia> @btime f1($args)
  352.910 ns (5 allocations: 80 bytes)
40.833333333333336
2 Likes