Passing Function As Object VS Creating New Function

Hello!

So, I’ve stumbled upon a performance difference between two situations, and I don’t understand the reason behind this difference. The difference in computation time occurs between 1) passing a function to a variable and 2) using a function as part of another function.

Here is what I mean:

Scenario 1
I create a function f, and then pass it to another variable g.

f(x) = x^2
g = f

When I evaluate g, for a certain value, I will get the following computation time data:

julia> @benchmark g(2.1)
BenchmarkTools.Trial: 10000 samples with 995 evaluations.
 Range (min … max):  23.920 ns … 581.970 ns  ┊ GC (min … max): 0.00% … 91.54%
 Time  (median):     25.078 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   26.954 ns ±   9.373 ns  ┊ GC (mean ± σ):  0.50% ±  1.57%

  ▅█▆▅▇▆▄▂           ▂▁▁▃▅▄▂▁▁▂▂▃▃▃▃▂▁                         ▂
  █████████▆▄▄▄▄▄▄▅▅███████████████████▇▆▄▆▃▄▃▄▃▃▂▂▄▄▅▅▅▆▅▅▄▃▄ █
  23.9 ns       Histogram: log(frequency) by time      39.4 ns <

 Memory estimate: 16 bytes, allocs estimate: 1.

Scenario 2
I create a function f, and then use it in the definition of a function h.

f(x) = x^2
h(x) = f(x)

When I evaluate h, for a certain value, I will get the following computation time data:

julia> @benchmark h(2.1)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  0.051 ns … 0.097 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     0.056 ns             ┊ GC (median):    0.00%
 Time  (mean ± σ):   0.059 ns ± 0.006 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

         ▆  ▃                                      ▆   █     
  ▃▁▁▇▁▁▁█▁▁█▁▁▁█▁▁▅▁▁▁▂▁▁▂▁▁▁▂▁▁▁▁▁▁▁▁▁▁▂▁▁▂▁▁▁▅▁▁█▁▁▁█▁▁▆ ▃
  051 ns         Histogram: frequency by time        067 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

Why is there such an important computation time difference between these two protocols?

Thank you!

f and h are functions. g is not a function but a global variable. This is the cause of the performance problem of g(2.1).

4 Likes

You could try

const j = f
@benchmark j(2.1) # as fast as f(2.1)

k(x, f) = f(x)
@benchmark k(2.1, f) # as fast as f(2.1)

Thus, the bad performance of g comes from its type instability as a global variable.

1 Like

0.05ns is not a real execution time. There is no function that executes that quickly. This is a benchmarking artefact.

3 Likes
using BenchmarkTools

f(x) = x^2
g = f
const g_const = f

F(x) = f(x)
G(x) = g(x)
G_const(x) = g_const(x)

@btime f(2.1)
@btime F(2.1)
@btime G(2.1)
@btime G_const(2.1)
println()
@btime g(2.1)
@btime $g(2.1)
println()
@btime F($(Ref(2.1))[])
@btime G($(Ref(2.1))[])
@btime G_const($(Ref(2.1))[]);

Result:

  0.001 ns (0 allocations: 0 bytes)
  0.001 ns (0 allocations: 0 bytes)
  23.139 ns (2 allocations: 32 bytes)
  0.001 ns (0 allocations: 0 bytes)

  23.494 ns (1 allocation: 16 bytes)
  0.001 ns (0 allocations: 0 bytes)

  1.400 ns (0 allocations: 0 bytes)
  24.648 ns (2 allocations: 32 bytes)
  1.400 ns (0 allocations: 0 bytes)

The reason for the slowdown to about 23 ns is that g is a global variable, not the function f itself. The slowdown can be avoided by using const.

The cause of 0.001 ns is to “cheat” the benchmark by hoisting the calculation out of the benchmark code. I have used $(Ref (2.1))[] to avoid the “cheating”. See GitHub - JuliaCI/BenchmarkTools.jl: A benchmarking framework for the Julia language

Alright! Thanks everyone, that makes a lot of sense!