Warning says method definition is overwritten, but the overwritten function still works

I have a function foo which works without warning:

function foo(start::Union{T, Vector{T}}) where T <: Real
    f(i, x::T) = x
    f(i, x::Vector{T}) = x[i]

    output = rand(T, 10)

    for i in eachindex(output)
        output[i] = f(i, start)
    end

    return output
end

julia> foo(2.3) == foo(2.3 .* ones(10))
true

Now I made a function foobar, but it gave me a warning:

function foobar(start::Union{T, Vector{T}}) where T <: Real
    f(i, x::T = start) = x
    f(i, x::Vector{T} = start) = x[i]

    output = rand(T, 10)

    for i in eachindex(output)
        output[i] = f(i)
    end

    return output
end

WARNING: Method definition (::Main.var"#f#1"{T, start})(Any) where {T} in module Main at REPL[1]:2 overwritten at REPL[1]:3.

julia> foobar(2.3) == foobar(2.3 .* ones(10))
true

Why do I get a warning that the function f is overwritten even though the function still works as if nothing has been overwritten?

The warning is telling you that when you write

f(i, x::T = start) = x
f(i, x::Vector{T} = start) = x[i]

both lines define a single-argument method f(i) in addition to their respective two-argument methods. That’s the sense in which the second line overwrites the first. As it happens, they’re defining identical methods for f(i), so everything works the way you expect. Still, here’s how I’d recommend you express this to avoid the warning and be sure it always works like it’s supposed to:

f(i) = f(i, start)
f(i, x::T) = x
f(i, x::Vector{T}) = x[i]
3 Likes

Just to explain this a bit more: The definition

f(i, x::T = start) = x

expands to

f(i, x::T) = x
f(i) = f(i, start)

Thus, your two method definitions expand to

f(i, x::T) = x
f(i) = f(i, start)
f(i, x::Vector{T}) = x[i]
f(i) = f(i, start)

The overwritten method warning is due to the fourth line in this expansion overwriting the second.

Here’s the relevant manual section: Methods · The Julia Language

3 Likes

Why is there a very large performance difference between foo and foobar with the changes you suggested? Does it really take ~40x time to pick the correct function each time in the loop?

function foo(start::Union{T, Vector{T}}) where T <: Real
    f(i, x::T) = x
    f(i, x::Vector{T}) = x[i]

    output = rand(T, 10000000)

    for i in eachindex(output)
        output[i] = f(i, start)
    end

    return output
end
function foobar(start::Union{T, Vector{T}}) where T <: Real
    f(i) = f(i, start)
    f(i, x::T) = x
    f(i, x::Vector{T}) = x[i]

    output = rand(T, 10000000)

    for i in eachindex(output)
        output[i] = f(i)
    end

    return output
end
a = 2.3 .* ones(10000000);

julia> foo(2.3) == foo(a) == foobar(2.3) == foobar(a)
true

using BenchmarkTools

julia> @btime foo(a);
  32.238 ms (3 allocations: 76.29 MiB)

julia> @btime foobar(a);
  1.227 s (39998472 allocations: 686.62 MiB)

The immediate reason is that f is being boxed:

julia> @code_warntype foobar(a)
MethodInstance for foobar(::Vector{Float64})
  from foobar(start::Union{Vector{T}, T}) where T<:Real @ Main REPL[1]:1
Static Parameters
  T = Float64
Arguments
  #self#::Core.Const(Main.foobar)
  start::Vector{Float64}
Locals
  @_3::Union{Nothing, Tuple{Int64, Int64}}
  output::Vector{Float64}
  f@_5::Core.Box
  i::Int64
  f@_7::Union{}
  f@_8::Union{}
  f@_9::Union{}
  f@_10::Union{}
Body::Vector{Float64}
...

Exactly why this happens in foobar but not in foo, I do not fully understand. It could be an unfortunate side effect of how closures are implemented. E.g. because the set of captured variables (start and T) differs between the various f-methods.

Edit: it seems to be because f(i) calls f(i, something). Why it happens then, I don’t know.

Ah, what’s happening is that, in the method f(i) = f(i, start), f is boxed because it has multiple points of “assignment”. The different “assignments” are actually just different method definitions for the same function, but boxing happens at the level of parsing, which doesn’t understand such subtleties, and once the parser has done its rewriting, there’s no going back. That’s also why the original example doesn’t have this problem. When you write

f(i, x::T = start) = x
f(i, x::Vector{T} = start) = x[i]

the parser doesn’t see any method definitions where f calls itself (even though such a method is actually created at a later stage in the pipeline), so it’s not tempted to introduce any boxing.

Here’s one ugly solution: make the single-argument method a different function, and introduce a dummy variable that’s only assigned once to reference the two-argument function:

julia> function foobar(start::Union{T, Vector{T}}) where T <: Real
           f(i, x::T) = x
           f(i, x::Vector{T}) = x[i]
           _f = f
           g(i) = _f(i, start)

           output = rand(T, 10000000)

           for i in eachindex(output)
               output[i] = g(i)
           end

           return output
       end
foobar (generic function with 1 method)

julia> @btime foobar($(rand(10000000)));
  8.818 ms (3 allocations: 76.30 MiB)

I think this solution is nicer, though:

function foobar(start::Union{T, Vector{T}}) where T <: Real
    f(i, x = start) = x
    f(i, x::Vector) = x[i]

    output = rand(T, 10000000)

    for i in eachindex(output)
        output[i] = f(i)
    end

    return output
end

It’s perhaps a tad confusing that the default argument is not specific to the method definition it’s used in, but I think dropping the redundant type parameter T makes it a bit clearer because then the method definition that provides the default also has the form of a fallback catch-all method.

That’s the part I don’t understand. Why would it consider the method definitions multiple assignments? It might be that the parser can’t determine that the call to f is a call to a local function, but suspects it might be a global function?

In this example, f is boxed in g1, but not in g2:

function g1(x)
    h(i) = f(i)
    f(i) = i+1
    f(x)
end

function g2(x)
    f(i) = i+1
    h(i) = f(i)
    f(x)
end

@code_warntype g1(1)
@code_warntype g2(1)

This isn’t great behaviour.

Yeah, but that’s not really any different from this:

function g1(x)
    h(i) = y
    y = x + 1
    f(0)
end

function g2(x)
    y = x + 1
    h(i) = y
    f(0)
end

@code_warntype g1(1)  # boxed
@code_warntype g2(1)  # not boxed

Assignment after the closure definition triggers boxing, because the parser doesn’t know whether or not h will be called both before and after that assignment (say, in another task). A single assignment before the closure definition seems to be the only instance in which the parser feels safe.

Why method definition is treated like an assignment is a mystery, though. Even if you were mucking with the methods of f in between calls to h, the invalidation mechanism takes care of that; boxing is irrelevant (the function instance referenced by f never changes). Assignment vs. method definition is easy to distinguish at the parser level, it’s just checking whether the LHS is a symbol or a call expression (presumably indexing expressions on the LHS are already filtered out).