A question about redefining a function in Julia

I think “capturing a name within a function” is somewhat a nontrivial thing.

A safer design would be bypassing “capture and then re-bind” completely, e.g.

julia> const ref = Ref{Function}(x -> x+1);

julia> function g(; ref = ref)
           f = ref.x
           f(1)
       end;

julia> g()
2

julia> setfield!(ref, :x, x -> x+2);

julia> g()
3

Edit: the ref = ref in the definition of g is redundant, since the ref is already const, which is fast.

Oh my god. I didn’t think of this. Did you mean that

function f(; x = x/2, y = x)
    @show x y    
end

boils down to

function f()
    x = x/2
    y = x
    @show x y    
end

? If this is the case, I think it’s terrible. Because the first version looks clean and I think it will be clean. The second version apparently violates the avoid untyped global variables performance tip.


I asked AI to give me a reformulation of

function f(w; x = x, y = y)
    return w, x, y
end

, who gives me

function f(w; kwargs...)
    local x = haskey(kwargs, :x) ? kwargs[:x] : Main.x
    local y = haskey(kwargs, :y) ? kwargs[:y] : Main.y
    return w, x, y
end

So I guess that the definition f(w; x = x, y = y) is problematic in terms of performance.

There are many issues in this context…

But whatever, for writing daily code, there is only one thing important for me to care about:
This “const and no arg” style is already fast:

const x = rand(1000)
function loop_over_global()
    s = 0.0
    for i in x
        s += i
    end
    return s
end
loop_over_global()

There is no need to write a standard function like the following

function loop_over_global(x)
    ...
end
loop_over_global(x)

because this style in practical code would become very cumbersome that one can ill afford.

I did a test, which proves that “const” is the fastest, even faster than the standard definition.

test
import Statistics

a = rand(100000);
b = rand(100000);
c = rand(100000);
d = rand(100000);
e = rand(100000);
f = rand(100000);
g = rand(100000);
h = rand(100000);
i = rand(100000);
j = rand(100000);
k = rand(100000);
l = rand(100000);
m = rand(100000);
n = rand(100000);
o = rand(100000);
p = rand(100000);
q = rand(100000);
r = rand(100000);
s = rand(100000);

function loop_over_global(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
    s0 = -1.797693134862315e308
    for vec = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s), ind in vec
        s0 += ind
    end
    return s0
end

loop_over_global(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
tvec = [];
for _ = 1:100
    t = time()
    loop_over_global(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
    push!(tvec, time()-t)
end
Statistics.mean(tvec)

It has its uses. Say you create a module with some computation which may take as input a precision, and uses that to compute the number of basis functions used in the computation. However, not being infallible you allow for the caller to specify the number of basis functions. Moreover, this is not typically done, so you let these arguments be keyword arguments. You’d do something like this:

function computation(x, y; precision=√eps(x), Nbasis=nfuncs(precision))
    ...
end

The caller of the function may not be aware of the function nfuncs, it’s an implementation detail. So the caller may happen to define their own nfuncs for some entirely unrelated work. You wouldn’t want that to interfere with the default number of basis functions, the default should use your nfuncs (and and eps).

2 Likes

How would you interpret

function f(; x = x/2, y = x)
    @show x y    
end

other than using some outer variable x for computing x/2?

Potentially because of how the outer x and y are defined (like you mention), but not due to the haskey etc., as you can easily benchmark. The reason is that kwargs will be a (Pairs wrapper around a) NamedTuple for which the haskey and getindex (getfield) can just be compiled away.

Yes, I understand it now.

That grammer is indeed a bit tricky, which can be understood as x = x/2; y = x rather than
x, y = x/2, x.


According to my benchmark (with the instance attached in my #23 post), the performance of
the style “f(; x = x, y = y)” is virtually the same as the standard style (where you explicitly pass all args), their results are around 0.00163 sec on my computer.

Whereas the “const + no arg” style is not only a lot more concise to read but also has shorter time, which is 6.81496e-5 sec on my computer.

Maybe you’re right.

As far as I can tell you only provided code for your loop_over_global(<19 non-const positional arguments>) timing?

For the 0/1 (kw)arg version, i.e. with const x = rand(1000) and

  • function loop_over_global() ... end
  • function loop_over_global(x) ... end
  • function loop_over_global(; x = x) ... end

all of

  • @benchmark loop_over_global()
  • @benchmark loop_over_global(x)
  • @benchmark loop_over_global($x)
  • @benchmark loop_over_global(; x)
  • @benchmark loop_over_global(; x=$x)

(tested at the appropriate time) give me the same result (namely 910 ns mean execution time).

1 Like

Thank you for really testing that. I didn’t use any benchmark packages but just use practical experience: I execute in my shell (zsh in linux).

The three source code are:

kwarg.jl

import Statistics
import Random
Random.seed!(1)

a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s = (
    rand(1000) for _ = 1:19
);

function loop_over_global(; a = a, b = b, c = c, d = d, e = e, f = f, g = g, h = h, i = i, j = j, k = k, l = l, m = m, n = n, o = o, p = p, q = q, r = r, s = s)
    s0 = -1.797693134862315e308
    for vec = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s), ind in vec
        s0 += ind
    end
    return s0
end;

loop_over_global()
tvec = [];
for _ = 1:10000
    t = time()
    loop_over_global()
    push!(tvec, time()-t)
end
println("kwarg_in_def> $(sum(tvec))")

arg.jl

import Statistics
import Random
Random.seed!(1)

a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s = (
    rand(1000) for _ = 1:19
);

function loop_over_global(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
    s0 = -1.797693134862315e308
    for vec = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s), ind in vec
        s0 += ind
    end
    return s0
end;

loop_over_global(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
tvec = [];
for _ = 1:10000
    t = time()
    loop_over_global(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
    push!(tvec, time()-t)
end
println("standard_arg> $(sum(tvec))")

const.jl

import Statistics
import Random
Random.seed!(1)

const a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s = (
    rand(1000) for _ = 1:19
);

function loop_over_global()
    s0 = -1.797693134862315e308
    for vec = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s), ind in vec
        s0 += ind
    end
    return s0
end;

loop_over_global()
tvec = [];
for _ = 1:10000
    t = time()
    loop_over_global()
    push!(tvec, time()-t)
end
println("const_no_arg> $(sum(tvec))")

As you can see, the standard grammar in arg.jl is very cumbersome. The const.jl is both clean and fast.
(One last thing to add, if you drop the const annotation in const.jl, the result is around 18 sec).

Edit: Oh, I see. We can make arg.jl be faster than adding const (I did that in constarg.jl) and get