Call function on vectors of mixed type (using `FunctionWrapper` and `Union`s)

Hi everyone and a happy new year!

In one part of my code I have a vector v whose elements have different types.
Additionally, I have a function f with different methods for all the involved types and want to call this function on the elements of the vector, let’s say I want to compute sum(f(x) for x in v).
What is the best/fastest way to do this?

There are three things I have tried so far

  1. Just leaving v as it is as a Vector{Any}
  2. Transforming v to a union typed vector of type Union{unique(typeof.(v))...}
  3. Using the FunctionWrapper.jl package (on either the normal or the union typed vector which doesn’t seem to make a difference).

What I observe from these three cases is that the implementation with the union type vector is the fastest for up to three different types and computation time jumps by a factor 1000 for four different types (I guess this has something to do with the union type splitting limit I read about elsewhere).
The time used by the implementation using FunctionWrapper, on the other hand, is independent of the number of different types but is a factor of 6 slower than the union type implementation for 1–3 different types (making it by far the fastest for more different types).

So I guess in the in the end I have three questions:

  1. Most importantly, is there a way to further improve performance in such cases, especially for the case of many different types (an example code is appended below)?
  2. Since I like to understand the code I write, could someone maybe explain to me what FunctionWrapper actually does?
  3. I would also like to understand why I observe what I observe. Why is there this sharp increase in computation time for the Union type vector after more than three different types and why is my FunctionWrapper implementation for only one type slower than the “naive” implementation?

I am grateful for answers to any of these questions!

Here is some example code I used to benchmark this

using FunctionWrappers: FunctionWrapper
using BenchmarkTools

for i in 1:100
    str = """
    struct X$i 
        x::Int
    end"""
    include_string(Main, str)
end

for i in 1:100
    y = rand()
    string = """
    g(x::X$i, z::Int) = (x.x + $y) * z
    """
    include_string(Main, string)
end

function make_random_vector(len, nTypes)
    vals = rand(1:100, len)
    types = rand(1:nTypes, len)
    str(i) = "X$(types[i])($vals[$i])"
    return [eval(Meta.parse(str(i))) for i in eachindex(vals)] 
end


function benchmark_trial(len, maxNTypes)
    vecTimes = []
    unionTimes = []
    wrappedTimes = []
    wrappedUnionTimes = []
    for i in 1:maxNTypes
        vect = make_random_vector(len, i)
        unionType = Union{unique(typeof.(vect))...}
        unionVec::Vector{unionType} = Vector{unionType}(vect)
        wrapped =  [FunctionWrapper{Float64, Tuple{Int}}(y->g(x,y)) for x in vect]
        wrappedUnion =  [FunctionWrapper{Float64, Tuple{Int}}(y->g(x,y)) for x in unionVec]
        
        push!(vecTimes, mean(@benchmark sum(g(x,5) for x in $vect)))
        push!(unionTimes, mean(@benchmark sum(g(x,5) for x in $unionVec)))
        push!(wrappedTimes, mean(@benchmark sum(h(5) for h in $wrapped)))
        push!(wrappedUnionTimes, mean(@benchmark sum(h(5) for h in $wrappedUnion)))
    end
    return vecTimes, unionTimes, wrappedTimes, wrappedUnionTimes
end

benchmarkdata = benchmark_trial(100, 20)

using Plots
benchmarkdata[1][1].time
plot([[benchmarkdata[j][i].time for i in 1:20] for j in 1:4], 
    yscale=:log10, 
    label = ["Vector" "UnionVector" "Wrapped" "Wrapped Union"],
    xlabel = "Number of Types",
    ylabel = "time_ns"
    )