FunctionWrapper allocating when called

I made the following test code to check some allocations going around while using FunctionWrappers on a piece of code I have.

using FunctionWrappers
import FunctionWrappers: FunctionWrapper
using BenchmarkTools

T = Float64
f(x) = x[1]*x[2]

wrapf = FunctionWrapper{T, Tuple{Tuple{T,T}}}(f)
p = (1.0, 2.0)
wrapf(p)
@btime wrapf($p)
# 39.650 ns (2 allocations: 48 bytes)

I’m trying to figure out where these 2 allocations come from, but I’m unaware of what the issue is.

julia> @btime $wrapf($p)
  8.400 ns (0 allocations: 0 bytes)
2.0

You need to also do $ on wrapf.

4 Likes

Shouldn’t I only be interpolating the input variables? Is the $ in wrap needed because the function returns a value?

It’s for interpolating variables into the expression - wrapf is its own variable. If it was a const, it’d be fine:

julia> const _wrapf = wrapf
FunctionWrapper{Float64, Tuple{Tuple{Float64, Float64}}}(Ptr{Nothing} @0x0000016e4563e250, Ptr{Nothing} @0x0000016e44170030, Base.RefValue{typeof(f)}(f), typeof(f))

julia> @btime _wrapf($p)
  8.200 ns (0 allocations: 0 bytes)
2.0
5 Likes

Well, that makes sense. However, if I wrap the code in a function, I think I should be free of that extra allocation but it still remains.

function main()
	T = Float64
	f(x) = x[1] * x[2]

	wrapf = FunctionWrapper{T,Tuple{Tuple{T,T}}}(f)
	p = (1.0, 2.0)
	x = wrapf(p)
	return nothing
end

@btime main()

Because FunctionWrapper seems to allocate.

julia> function main()
           T = Float64
           f(x) = x[1] * x[2]
           wrapf = FunctionWrapper{T,Tuple{Tuple{T,T}}}(f)
       end
main (generic function with 1 method)

julia> @benchmark main()
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):   7.900 ns … 601.800 ns  β”Š GC (min … max): 0.00% … 0.00%
 Time  (median):     21.800 ns               β”Š GC (median):    0.00%
 Time  (mean Β± Οƒ):   25.099 ns Β±  12.977 ns  β”Š GC (mean Β± Οƒ):  0.00% Β± 0.00%

             β–†β–ˆ
  β–ƒβ–ƒβ–β–β–β–β–β–β–β–ƒβ–ˆβ–ˆβ–ˆβ–ˆβ–‡β–…β–…β–…β–„β–„β–„β–ƒβ–ƒβ–ƒβ–‚β–‚β–‚β–‚β–‚β–‚β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β– β–‚
  7.9 ns          Histogram: frequency by time         69.9 ns <

 Memory estimate: 56 bytes, allocs estimate: 2.
1 Like

I think this might be because you’re passing the type T as an object and Julia avoids specializing on it. If you try main(::Type{T}) where {T} the results could be different?
https://docs.julialang.org/en/v1/manual/performance-tips/#Be-aware-of-when-Julia-avoids-specializing

1 Like

The allocations remain even with ::Type{T}.

I have also tried with some of the examples in FunctionWrappers.jl’s tests and they have the same allocation behaviour. It seems there are always allocations when the wrapper is created.

Since T is defined in the body as a constant value I don’t think that applies here. Looking at @code_llvm it seems to know to define doubles in either case.

A FunctionWrapper is a mutable struct with two untyped fields. There’s probably no way to avoid a few allocations when instantiating one. See FunctionWrappers.jl/src/FunctionWrappers.jl at 3a579da1e5080d4563b6e819f88f4525aff4f45d Β· yuyichao/FunctionWrappers.jl Β· GitHub

3 Likes

Thank you all for your i put. I think yhese are allocations to live with.

I took a look at some SciML code where these wrappers are used and got to the conclusion that they are used of, say, a ODEProblem, and one just pays the allocation cost once.