World Age - Metaprogramming and Function Wrappers


#1

This question is related to this post and I am trying to understand how FunctionWrappers avoids the world age problem (using cfunction) and if there is a better way to realize the same functionality in a straightforward manner. I am using Julia v0.6 but feel free to point out if there might be any incompatibilities in the future.

Following is a simplified version of the problem I am trying to solve. I need to generate functions Int64 -> Int64, given a Vector{Int64} as an input that add the sum of the vectors to the input argument.

I define a compile method to build these functions and store them in an object that I can use in other parts of my code. The code works without any issues if I specify the type of the generated function using FunctionWrappers as invoker::FunctionWrappers.FunctionWrapper{Int64, Tuple{Int64}} (see GoodCompiledObject). The exact same code without this line causes a world age issue.

struct BadCompiledObject
    invoker
end

@inline (x::BadCompiledObject)(y) = x.invoker(y)

function compilebad(x::Vector{Int64})
    modulecode = quote
        function foo(x::Int64)
            x + sum($x)
        end

        @inline invokefoo() = foo
    end
    moduleexpr = Expr(:module, true, gensym(:CompiledObject), modulecode)
    eventmodule = eval(moduleexpr)

    BadCompiledObject(Base.invokelatest(eventmodule.invokefoo))
end

function profile_bad()
    x = [1, 10]
    bco = compilebad(x)
    @allocated bco(2)
end

profile_bad() # ERROR
# MethodError: no method matching foo(::Int64)
# The applicable method may be too new: running in world age 21945, while current world is 21949.e[0m
# Closest candidates are:
#   foo(::Int64) at profile.jl:286 (method too new to be called from this world context.)
# #16#f at util.jl:328 [inlined]
# macro expansion at util.jl:331 [inlined]
# profile_bad() at profile_nn.jl:307
# include_string(::String, ::String) at loading.jl:522
# include_string(::String, ::String, ::Int64) at eval.jl:30
# include_string(::Module, ::String, ::String, ::Int64, ::Vararg{Int64,N} where N) at eval.jl:34
# (::Atom.##102#107{String,Int64,String})() at eval.jl:82
# withpath(::Atom.##102#107{String,Int64,String}, ::String) at utils.jl:30
# withpath(::Function, ::String) at eval.jl:38
# hideprompt(::Atom.##101#106{String,Int64,String}) at repl.jl:67
# macro expansion at eval.jl:80 [inlined]
# (::Atom.##100#105{Dict{String,Any}})() at task.jl:80

###############################################################################
# Version that works
###############################################################################
import FunctionWrappers

struct GoodCompiledObject
    invoker::FunctionWrappers.FunctionWrapper{Int64, Tuple{Int64}}
end
@inline (x::GoodCompiledObject)(y) = x.invoker(y)

function compilegood(x::Vector{Int64})
    modulecode = quote
        function foo(x::Int64)
            x + sum($x)
        end

        @inline invokefoo() = foo
    end
    moduleexpr = Expr(:module, true, gensym(:CompiledObject), modulecode)
    eventmodule = eval(moduleexpr)

    GoodCompiledObject(Base.invokelatest(eventmodule.invokefoo))
end

function profile_good()
    x = [1, 10]
    gco = compilegood(x)

    @allocated gco(2)
end

profile_good() # returns 0

Following are my questions:

  • How come the world age issue doesn’t affect FunctionWrappers? Is it working around the dynamic dispatch pertaining to world age somehow?
  • Are there any performance overheads of using FunctionWrappers? My application is very performance critical and therefore is sensitive to even small overheads.
  • Is this the recommended way of achieving my end goal? My end goal is to generate functions that perform matrix operations on static arrays efficiently.
  • Will this approach work in Julia 1.0 as well?

#2

Perhaps this is a silly question, but why all this complexity when a closure would work instead? For example:

function make_foo(x)
  let s = sum(x)
    y -> y + s
  end
end

(The let block is not necessary for correctness but may help avoid type instabilities due to issue 15276)

This is far less code and will actually be faster than any cfunction solution, as the anonymous function can be inlined by the compiler.


#3

Closure is a perfectly good solution for the toy example I provided. But the problem I am trying to solve is a bit complicated and unfortunately I cannot share all the code here.

At a higher level, I have to do a bunch of matrix operations with all but one known ahead of time. These matrices are not particularly large in size and therefore I am trying to use static arrays to compile my matrix operations down to optimal vectorized code. Also, the class of models is not small enough for me to write individual closures for all possible cases.


#4

So far, nothing you have described indicates that there is any need for Function Wrappers nor any eval-stuff.

Could you give a representative example of what you want to do?


#5

Perhaps what you’re actually looking for is https://docs.julialang.org/en/v1/manual/metaprogramming/index.html#Generated-functions-1 ?