Efficient eval, splat, eval?

Essentially, I’d like to perform the following snippet of code without allocating or creating an intermediate array (temp):

x = Int[1,2]        # could be of any length n (n is only known at runtime)
temp = f.(x)      # f has one output and one input
y = g(temp...)   # g has n inputs and one output

Currently, I have the following function which allocates due to the call to ntuple (with n known at runtime only).

function q(f, g, x)
    g(ntuple(i -> f(x[i]), length(x))...)
end

Is there a better way to do this that I’m missing?

The most general answer is that it is hard to keep that non-allocating except in specific cases, because of the unknown size of the tuples. ntuples are the best choice in general. But I am not sure if there is a general solution for that.

Depending on the specifities of your case, you may find ways to tell the compiler which is the size of the vectors, such that those allocations disappear. For example, this is a trick:

julia> f(x) = x + 1
f (generic function with 1 method)

julia> g(args...) = sum(args)
g (generic function with 1 method)

julia> struct Vec{N,T} # put the size on the signature
         x::Vector{T}
       end

julia> Base.getindex(v::Vec,i) = v.x[i]

julia> x = Vec{2,Int}([1,2])
Vec{2, Int64}([1, 2])

julia> function q2(f, g, x::Vec{N,T}) where {N,T} # N will be known at compile time
           g(ntuple(i -> f(x[i]), N)...)
       end
q2 (generic function with 1 method)

julia> @btime q2($f,$g,$x2)
  1.691 ns (0 allocations: 0 bytes)
5


But that means that a specialized version of q2 will be compiled for every size, so that makes sense only if the sizes do not vary much.

It is generally considered bad practice to use splatting for dynamically sized objects and because of how method dispatch works, something like this will always incur a large overhead. I would instead try to write g in such a way that it accepts a single vector instead of a variable number of arguments.

2 Likes