Julia: function types and performance

The reason is that x -> sqrt(x) is defined as a new anonymous function with each call to compute_sum2, so this causes new compilation every time you call it.

If you define it before even e.g. like this:

julia> f = x -> sqrt(x)

then you have:

julia> @time compute_sum2(xs, f) # here you pay compilation cost
  0.010053 seconds (19.46 k allocations: 1.064 MiB)
665.2469135020949

julia> @time compute_sum2(xs, f) # here you have already compiled everything
  0.000003 seconds (1 allocation: 16 bytes)
665.2469135020949

Note that a natural approach would be to define a function with a name like this:

julia> g(x) = sqrt(x)
g (generic function with 1 method)

julia> @time compute_sum2(xs, g)
  0.000002 seconds
665.2469135020949

You can see that x -> sqrt(x) defines a fresh anonymous function each time it is encountered when you write e.g.:

julia> typeof(x -> sqrt(x))
var"#3#4"

julia> typeof(x -> sqrt(x))
var"#5#6"

julia> typeof(x -> sqrt(x))
var"#7#8"

Note that this would be different if an anonymous function would be defined in a function body:

julia> h() = typeof(x -> sqrt(x))
h (generic function with 2 methods)

julia> h()
var"#11#12"

julia> h()
var"#11#12"

julia> h()
var"#11#12"

and you see that this time the anonymous function is the same every time.


In addition to the excellent response by Bogumil, I would just like to add that a very convenient way of generalizing this is to use the normal functional programming function like map, reduce, fold, etc.

In this case, you're doing a map transformation (namely sqrt) and a reduce (namely +), so you can also achieve the result with mapreduce(sqrt, +, xs). This has essentially no overhead and is comparable to a manual loop in performance.

If you have a really complicated series of transformations, you can get optimal performance and still use a function using the Transducers.jl package.


The other answers are pretty comprehensive, but I wanted to point out that you can just omit the square brackets from sum([sqrt(x) for x in xs]) and get the fastest version of all:

julia> using BenchmarkTools

julia> @btime compute_sum($xs)
  1.779 μs (0 allocations: 0 bytes)
679.0943275393031

julia> @btime sum([sqrt(x) for x in $xs])
  1.626 μs (1 allocation: 7.94 KiB)
679.0943275393028

julia> @btime sum(map(sqrt, $xs))
  1.628 μs (1 allocation: 7.94 KiB)
679.0943275393028

julia> @btime sum(sqrt(x) for x in $xs)
  1.337 μs (0 allocations: 0 bytes)
679.0943275393031

Also note that on my computer on Julia master compute_sum is the slowest, not the fastest way to sum these numbers.


Bogumił has already answered the part about the function type. I want to point out that your implementation is already working as efficiently as possible, if benchmarked properly, but can be replaced by equivalent built-in functions:

julia> @btime compute_sum($xs)
  2.149 μs (0 allocations: 0 bytes)
661.6571623823567

julia> @btime sum(sqrt, $xs)
  2.149 μs (0 allocations: 0 bytes)
661.6571623823567

julia> @btime compute_sum2($xs, sqrt)
  2.149 μs (0 allocations: 0 bytes)
661.6571623823567

julia> @btime mapreduce(sqrt, +, $xs)
  2.149 μs (0 allocations: 0 bytes)
661.6571623823567

And it is always better to use an eta-equivalent non-lambda function, if possible: f instead of x -> f(x). Especially for builtin functions, since they are sometimes dispatched on.