Type instability seemingly due to redundant inner anonymous functions that are never called

I have created an MWE that can readily executed by copy-pasting to the REPL, provided package DispatchDoctor.jl is installed.

What follows below is:

  • A variable Z is created in global scope (see end of script below).

  • A function mwe that, according to DispatchDoctor.jl, exhibits type instability when called with mwe(Z). The instability comes from an inner function called lowerbound. Function lowerbound defines in its body two anonymous functions that are never called.

  • A function mwe_2 that is identical to mwe, but where the first anonymous function that is never called is now commented out. When called with mwe_2(Z), no type instability is detected.

  • A function mwe_3 that is identical to mwe, but where the second anonymous function that is never called is now commented out. When called with mwe_3(Z), no type instability is detected.


My question: why do the two redundant anonymous function influence the type instability?


using DispatchDoctor

function mwe(Z)

    local kernel(xᵢ, xⱼ, σf, ℓ) = σf * exp( - abs2(xᵢ - xⱼ) * ℓ)

    @stable function lowerbound(σf, ℓ)

        local k(x, z) = kernel(x, z, σf, ℓ)  # this is never called

        local k(x) = kernel.(x, Z, σf, ℓ)

        local s(x) = dot(k(x), k(x)) # this is never called

        return k(1.0)

    end

    lowerbound(1.0, 1.0)

end


# same as mwe but line "local s(x) = dot(k(x), k(x))" is commented out
function mwe_2(Z) 

    local kernel(xᵢ, xⱼ, σf, ℓ) = σf * exp( - abs2(xᵢ - xⱼ) * ℓ)

   @stable function lowerbound(σf, ℓ)

        local k(x, z) = kernel(x, z, σf, ℓ)  # this is never called

        local k(x) = kernel.(x, Z, σf, ℓ)

        # local s(x) = dot(k(x), k(x)) # this is never called

        return k(1.0)

    end

    lowerbound(1.0, 1.0)

end


# same as mwe but line "local k(x, z) = kernel(x, z, σf, ℓ)" is commented out
function mwe_3(Z)

    local kernel(xᵢ, xⱼ, σf, ℓ) = σf * exp( - abs2(xᵢ - xⱼ) * ℓ)

    @stable function lowerbound(σf, ℓ)

        # local k(x, z) = kernel(x, z, σf, ℓ)  # this is never called

        local k(x) = kernel.(x, Z, σf, ℓ)

        local s(x) = dot(k(x), k(x)) # this is never called

        return k(1.0)

    end

    lowerbound(1.0, 1.0)

end


# I am aware that this is a *non-const* global
Z = collect(LinRange(0, 30, 30));

mwe(Z) # type instability detected
mwe_2(Z) # NO type instability detected
mwe_3(Z) # NO type instability detected
1 Like

A rough answer is that closures are currently implemented at an early phase of compilation, called lowering, and lowering runs before inference and optimization. So lowering is mostly just based on syntax. I don’t understand the specific reason here, but basically lowering gives up here and boxes k, causing bad type inference for the later compilation phases (k is inferred as Any, the least precise type).

Seems curious that this happens even though k is never (re)assigned.

Some links:

3 Likes

A possible workaround seems to be to define the two methods of k together in a let:

julia> using Cthulhu: @descend

julia> kernel(xᵢ, xⱼ, σf, ℓ) = σf * exp( - abs2(xᵢ - xⱼ) * ℓ)
kernel (generic function with 1 method)

julia> function mwe(Z)
           function lowerbound(σf, ℓ)
               k = let k(x, z) = kernel(x, z, σf, ℓ)
                   k(x) = kernel.(x, Z, σf, ℓ)
                   k
               end
               s(x) = dot(k(x), k(x))
               k(1.0)
           end
           lowerbound(1.0, 1.0)
       end
mwe (generic function with 1 method)

julia> Z = collect(LinRange(0, 30, 30));

julia> @descend remarks=true with_effects=true optimize=false mwe(Z)  # infers fine
2 Likes

BTW, off-topic comment, just regarding nomenclature: s and k both have names (“s” and “k”, respectively), so neither is an anonymous function. Example of an anonymous function: x -> 3 * x.

1 Like

Thanks for your answer. The links referred me to material that is too advanced for me, but helped me nevertheless understand a bit more the nature of the problem. I am surprised that a seemingly innocuous redundant code would lead to type instability. Once I corrected this issue in my actual code, I achieved a speed up of about 75-100%. I think I will start avoiding closures for now.

Thanks for taking the time to point this out, I was very sloppy indeed.

1 Like