To avoid allocations, avoid anonymous functions?

Because of habits from previous languages, I think, I was wanting to pass around an object (t) that would handle incoming data (“f”) based on parameters (in the example “d”). That was doing a lot of allocation. Based on my experimentation, it seems the pattern could be instead to pass around an indicator type and have method that accepts both the data (“x”) and the parameter (“d”). I did a fair bit of googling but couldn’t find a great explainer to fix my confusion. Care to share a link or advice?

struct MyType
    f
end

function make_mytype(d::Float64)
    return MyType(f::Float64->d+f)
end

function process_x(t::MyType, x::Float64)
    a = t.f(x)
    return nothing
end

myt = make_mytype(2.0)

process_x(myt, 1.0)

struct MyType2
end

function handle_mytype2(x::Float64, t::MyType2, d::Float64)
    return d + x
end

function process_x2(x, t, d)
    a = handle_mytype2(x, t, d)
    return nothing
end

myt2 = MyType2()

process_x2(1.0, myt2, 2.0)

Profile.clear_malloc_data()

@time process_x(myt, 1.0)

@time process_x2(1.0, myt2, 2.0)

With output

  0.000003 seconds (85 allocations: 6.123 KiB)
  0.000002 seconds (4 allocations: 160 bytes)

and Coverage analyze_malloc

julia> analyze_malloc(".")
3-element Array{Coverage.MallocInfo,1}:
 Coverage.MallocInfo(0, "./test.jl.mem", 11)
 Coverage.MallocInfo(16, "./test.jl.mem", 10)
 Coverage.MallocInfo(28716, "./test.jl.mem", 6)

That doesn’t have type-stable access.

struct MyType{F}
    f::F
end
1 Like

You might also want to think about making a callable type.

struct MyType{D}
    d::D
end
(M::MyType)(x) = M.d+x
1 Like

There is no performance penalty for anonymous functions at all (in fact, they are internally handled by the same mechanisms that handle “regular” functions). It’s just that you’re seeing measurements which are not particularly helpful because:

  1. Your f field is un-typed (as Chris mentioned). See: https://docs.julialang.org/en/stable/manual/performance-tips/#Avoid-fields-with-abstract-type-1
  2. You are timing in global scope. For accurate, representative benchmark results, especially for fast functions, use GitHub - JuliaCI/BenchmarkTools.jl: A benchmarking framework for the Julia language, e.g. @benchmark process_x2(1.0, $myt2, 2.0).
1 Like

Also, your process_x2 function returns nothing, which could potentially allow a sufficiently smart compiler to decide to do nothing at all (if it can prove that f() has no side-effects). See: https://github.com/JuliaCI/BenchmarkTools.jl/blob/master/doc/manual.md#understanding-compiler-optimizations

2 Likes

Thanks! This was very helpful. I thought I had read Performance Tips, but obviously not closely enough.