Fast evaluation of multiple functions on multiple arguments

In my application I often have a bunch of functions that I need to evaluate on many arguments (i.e. each combination of function and argument).
I thought of extending the Base.map function as follows:

function Base.map(F::AbstractArray{Function}, A::AbstractArray)
    [[f(a) for f in F] for a in A]
end

Although this produces the correct output, it is about 10 times slower than expected. On the other hand, the following

function Base.map(F::AbstractArray{Function}, A::AbstractArray)
    [Base.map(f, A) for f in F]
end

is as fast as it should be, but it does not have the desired output format (for my use).

  1. is the above extension of Base.map duplicating some existing function in julia?
  2. how do I perform the computation in roughly the same time, while obtaining the desired output format?
1 Like

You can just do what you want with the broadcasting syntax like this:

map.(F, Ref(A))

The Ref here is to make A a scalar with respect to broadcasting, so that it iterates map(f, A) over all f ∈ F.

If performance is important, you might want to consider using a tuple for the functions instead of an array, since Function is an abstract type and therefore, your function will not be type stable.

2 Likes

Could perhaps do something like:

julia> fs = [sin, cos, tan];

julia> args = [0.0, pi/2, pi];

julia> apply(f, x) = f(x);

julia> apply.(fs, permutedims(args))
3×3 Array{Float64,2}:
 0.0  1.0           1.22465e-16
 1.0  6.12323e-17  -1.0
 0.0  1.63312e16   -1.22465e-16

But I guess that gives you a matrix which you don’t want.

2 Likes

If one loop is fast and the other is slow, it makes a difference which one is the inner and which is the outer. Iterating over an array of Functions is possibly slow.

Maybe you should do the calculations with the fast inner loop, and then rearrange the output afterwards.

Thanks, this looks as if it might to the job!

Regarding type stability, you mean that I should define map to take as follows:

function Base.map(F::NTuple{N, Function}, A::AbstractArray) where N
    map.(F, Ref(A))
end

? Does it mean that the compiler will specialize for a specific set of functions?

In fact, if I do this, the result seems to be slightly faster even than [Base.map(f, A) for f in F] and there are no type warnings.

It’s generally bad practise to overload methods from Base for Base types, as this could have unexpected side effects.

Yes, this should happen. This also explains, why your second function was faster than the first, since map could specialize on each f individually, at least.

1 Like

I see. Unfortunately there are no existing methods for Base.map for tuples of functions (there is probably a good reason for this). The tradeoff I have is that I have to separately code function evaluation on a sample depending on whether the function is scalar- or vector-valued. If I only need the function for a custom type, the overloading should be fine. In the case where I need the function on arbitrary arrays, I could define Map = deepcopy(Base.map) and then overload the Map and only work with Map in all of my code. Or would there be an even simpler way?

Thanks! I’m sure this will also become useful at some point.

The way I proposed, with the dot syntax will work just fine, if F is a scalar, too.

You have to be careful with these types of function definitions, since non-const functions can negatively impact performance. If you really want to put it in its own function, I would just declare it just like any other function and give it a name, that makes the difference clear.

That is the last thing you need to worry about and the non-constness is also not related to copying. It could even be local for example.

However, that just won’t work. deepcopy only copy the value, it does not copy the type, what you’ll get is an object of the same type (typeof(x) === typeof(deepcopy(x)) should always be true) and since the method table is a property of the type, deepcopy will not change which method table you extend.

Thanks! I just noticed that the deepcopy construction is not going to work.
In the end, it seems the safest bet is to define a new function Map and call Base.map for those methods I will need and then define new methods for the function tuples on top of that.