JuliaDebug / Cthulhu.jl

The slow descent into madness
MIT License
657 stars 41 forks source link

Varargs invoke is inconsistently inferred #119

Open cafaxo opened 3 years ago

cafaxo commented 3 years ago

While investigating https://github.com/JuliaLang/julia/issues/39175 with Cthulhu 1.6 on https://github.com/JuliaLang/julia/commit/ffa966ee22, the following puzzled me:

Consider

julia> @noinline function f(x...)
       return x
       end;

julia> g(x) = f(x, 1);

julia> @code_warntype optimize=:true g(1)
Variables
  #self#::Core.Const(g)
  x::Int64

Body::Tuple{Int64, Int64}
1 ─ %1 = invoke Main.f(_2::Int64, 1::Vararg{Int64})::Tuple{Int64, Int64}
└──      return %1

This suggests that the invoked signature of f is (Int64, Vararg{Int64}). However, doing @descend g(1) and pressing o to get the optimized version yields

CodeInfo(
    @ REPL[2]:1 within `g'
1 ─ %1 = Main.f(x, 1)::Tuple{Int64, Int64}
└──      return %1
)
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [o]ptimize, [w]arn, [v]erbose printing for warntype code, [d]ebuginfo, [s]yntax highlight for Source/LLVM/Native.
Show: [S]ource code, [A]ST, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
Advanced: dump [P]arams cache.
 • %1  = f(::Int64,::Int64)::Tuple{Int64, Int64}

which suggests that the invoked signature is (Int64, Int64).

So which one is correct?

timholy commented 3 years ago

xref #92

I generally suspect the @code_typed is more reliable, but I'm not sure even that's always true.

cafaxo commented 3 years ago

Thanks. I have another (unrelated?) question:

After running

julia> @noinline function f(x...)
       return x
       end;

julia> g(x) = f(x, 1);

julia> g(1)
(1, 1)

I get

julia> methods(f).ms[1].specializations
svec(MethodInstance for f(::Int64, ::Int64), #undef, #undef, #undef, #undef, #undef, #undef, MethodInstance for f(::Int64, ::Vararg{Int64}))

Why is the specialization f(::Int64, ::Int64) created?

timholy commented 3 years ago

Great question, I've been (and sometimes still am) confused about the same thing. The short answer is that specialization of inference is distinct from specialization of codegen.

julia> @noinline function f(x...)
       return x
       end;

julia> g(x) = f(x, 1);

julia> g(1)
(1, 1)

julia> using MethodAnalysis

julia> mis = methodinstances(f)
2-element Vector{Core.MethodInstance}:
 MethodInstance for f(::Int64, ::Int64)
 MethodInstance for f(::Int64, ::Vararg{Int64, N} where N)

julia> mis[1].cache.specptr
Ptr{Nothing} @0x0000000000000000

julia> mis[2].cache.specptr
Ptr{Nothing} @0x00007f007fe42e10

It's the second one which actually runs. But it's been decided (and I generally agree) that it's useful for callers who know more about their argument types to be able to infer the return type. It also can help link, e.g., the callee of a do block to the method in which it's defined (why that might be important: https://julialang.org/blog/2021/01/precompile_tutorial/). You can generally use Base.inferencebarrier(arg) in the caller when you want to prevent even inference-specialization of the callee.