How to alias a function to have maximum speed

I have a function, say f(x,y,z)=x+y+z
In many situations, I would like to fix one of the parameters, say letting z=10, and give it another name.
To do this, I can think of several different ways.

g1(x,y)=f(x,y,10)
g2=f

function rename()
  g(x,y)=f(x,y,10)
  return g
end

g3=rename()

Then, calling g1(x,y), g2(x,y,10) and g3(x,y) should give the same result.

However, the time consuming for these three functions are very different.
A simple test suggest that g1 works far more faster than g2 and g3.

Why is that?
If I have to alias f using g2 or g3 way, how can I make it fast?

Don’t benchmark in global scope.

Inside a function (i.e. in local scope), they should all be basically the same in performance, and all fast, but the simplest and most idiomatic thing is generally to use an anonymous function (x,y) -> f(x,y,10).

2 Likes

This is my test code

function test1()
	for i=1:1000
		g1(i,i+1)
	end
end

function test2()
	for i=1:1000
		g2(i,i+1,10)
	end
end

function test3()
	for i=1:1000
		g3(i,i+1)
	end
end

@time test1()
@time test2()
@time test3()

and the result is:

0.000001 seconds
0.006006 seconds (1.75 k allocations: 28.250 KiB, 98.73% compilation time)
0.004733 seconds (3.35 k allocations: 124.493 KiB, 97.85% compilation time)

The problem is that g2 and g3 are variables in global scope. You either need to define them in the respective functions (test2 and test3), or declare them constant (const g2=f, const g3=rename()).

Also, to time your functions, consider using @btime from BenchmarkTools instead of @time, which includes the compilation time.

5 Likes

Benchmarking can be difficult, and here the compiler tricks you.

julia> using BenchmarkTools

julia> @btime test1()
  1.200 ns (0 allocations: 0 bytes)

That’s unrealistically fast. What if we run the loop a trillion times?

function test1t()
	for i=1:10^12
		g1(i,i+1)
	end
end

julia> @btime test1t()
  1.200 ns (0 allocations: 0 bytes)

Still 1.2ns. The compiler realizes that your test function returns nothing, and has no observable effects, and can therefore be replaced with test1() = nothing.

Running a lot of iterations of a loop, for benchmarking purposes, will just trick you. Leave this kind of looping to BenchmarkTools. And do make sure that your benchmarked function returns something that can be observed, otherwise the compiler will go behind your back and replace the function with nothing at all.

7 Likes

Why do you want another function name at all? You can use default arguments for that. And if you find it difficult to remember argument names/order, use optional named arguments in this case f(; x=1, y=2, z=3).

julia> f(x=1, y=2, z=3) = x + y + z;

julia> f()
6

julia> f(1)
6

julia> f(1, 2)
6

julia> f(1, 2, 3)
6

Another option is to define multiple methods for f:

julia> f(x, y, z) = x + y + z
f (generic function with 1 method)

julia> f(x, y) = f(x, y, 3)
f (generic function with 2 methods)

julia> f(x) = f(x, 2, 3)
f (generic function with 3 methods)

julia> f() = f(1, 2, 3)
f (generic function with 4 methods)

julia> f()
6

julia> f(1)
6

julia> f(1,2)
6

julia> f(1,2,3)
6

It’s quite common to want to fix certain arguments at the call site when a function is passed to some higher-order function (e.g. optimization, map, etcetera), rather than at the function-definition site (which you may not even control).

4 Likes

Makes sense. In this case, using an anonymous function (x,y) -> f(x,y,10) as you suggested would be perfect. But the OP didn’t explicitly specify their real use case.

1 Like

Thank you for your advice!

After adding the const keyword, g2 and g3 do get as fast as g1.