Strange type instability

I’m encountering a strange type instability that I could distill to the following minimal code:

function unstable(rv,x0)
    x1=copy(x0)
    for t in 1:10
        p::Int = prod(x0[j] for j in rv; init=1)
        x1[1] = p == 1 ? 0 : 1
        x0,x1 = x1,x0
    end
    return x0
end

@code_warntype gives the following. The instability disappears if:

  • If I set p=1 instead of the p=prod(... )
  • If I typeassert x0 and x1 in the assignment, i.e. x0::Vector{Int},x1::Vector{Int} = x1,x0 instead of x0,x1 = x1,x0.

Any idea or explanation?

julia> @code_warntype unstable(fill(1,10),fill(1,10))
MethodInstance for unstable(::Vector{Int64}, ::Vector{Int64})
  from unstable(rv, x0) @ Main REPL[103]:1
Arguments
  #self#::Core.Const(unstable)
  rv::Vector{Int64}
  x0@_3::Vector{Int64}
Locals
  @_4::Union{Nothing, Tuple{Int64, Int64}}
  x1::Any
  #109::var"#109#110"
  t::Int64
  p::Int64
  x0@_9::Union{}
  x0@_10::Union{}
  x0@_11::Union{}
  x0@_12::Union{Core.Box, Vector{Int64}}
  @_13::Int64
Body::Any
1 ──       (x0@_12 = x0@_3)
β”‚          (x0@_12 = Core.Box(x0@_12::Vector{Int64}))
β”‚          Core.NewvarNode(:(@_4))
β”‚          Core.NewvarNode(:(x1))
β”‚    %5  = Core.isdefined(x0@_12::Core.Box, :contents)::Bool
└───       goto #3 if not %5
2 ──       goto #4
3 ──       Core.NewvarNode(:(x0@_9))
└───       x0@_9
4 ┄─ %10 = Core.getfield(x0@_12::Core.Box, :contents)::Any
β”‚          (x1 = Main.copy(%10))
β”‚    %12 = (1:10)::Core.Const(1:10)
β”‚          (@_4 = Base.iterate(%12))
β”‚    %14 = (@_4::Core.Const((1, 1)) === nothing)::Core.Const(false)
β”‚    %15 = Base.not_int(%14)::Core.Const(true)
└───       goto #13 if not %15
5 ┄─ %17 = @_4::Tuple{Int64, Int64}
β”‚          (t = Core.getfield(%17, 1))
β”‚    %19 = Core.getfield(%17, 2)::Int64
β”‚          (#109 = %new(Main.:(var"#109#110"), x0@_12::Core.Box))
β”‚    %21 = #109::var"#109#110"
β”‚    %22 = Base.Generator(%21, rv)::Base.Generator{Vector{Int64}, var"#109#110"}
β”‚    %23 = (:init,)::Core.Const((:init,))
β”‚    %24 = Core.apply_type(Core.NamedTuple, %23)::Core.Const(NamedTuple{(:init,)})
β”‚    %25 = Core.tuple(1)::Core.Const((1,))
β”‚    %26 = (%24)(%25)::Core.Const((init = 1,))
β”‚    %27 = Core.kwcall(%26, Main.prod, %22)::Any
β”‚    %28 = Base.convert(Main.Int, %27)::Any
β”‚          (p = Core.typeassert(%28, Main.Int))
β”‚    %30 = (p == 1)::Bool
└───       goto #7 if not %30
6 ──       (@_13 = 0)
└───       goto #8
7 ──       (@_13 = 1)
8 ┄─ %35 = @_13::Int64
β”‚          Base.setindex!(x1, %35, 1)
β”‚    %37 = x1::Any
β”‚    %38 = Core.isdefined(x0@_12::Core.Box, :contents)::Bool
└───       goto #10 if not %38
9 ──       goto #11
10 ─       Core.NewvarNode(:(x0@_10))
└───       x0@_10
11 β”„ %43 = Core.getfield(x0@_12::Core.Box, :contents)::Any
β”‚          Core.setfield!(x0@_12::Core.Box, :contents, %37)
β”‚          (x1 = %43)
β”‚          (@_4 = Base.iterate(%12, %19))
β”‚    %47 = (@_4 === nothing)::Bool
β”‚    %48 = Base.not_int(%47)::Bool
└───       goto #13 if not %48
12 ─       goto #5
13 β”„ %51 = Core.isdefined(x0@_12::Core.Box, :contents)::Bool
└───       goto #15 if not %51
14 ─       goto #16
15 ─       Core.NewvarNode(:(x0@_11))
└───       x0@_11
16 β”„ %56 = Core.getfield(x0@_12::Core.Box, :contents)::Any
└───       return %56

That’s the infamous Julia#15276. x0[j] for j in rv is a closure that captures x0, but x0 has complicated lifetime and gets re-assigned. You can fix it (and remove the ::Int assertion) with:

p = let x0=x0; prod(x0[j] for j in rv; init=1); end
full code
julia> function stable(rv,x0)
           x1=copy(x0)
           for t in 1:10
               p = let x0=x0; prod(x0[j] for j in rv; init=1); end
               x1[1] = p == 1 ? 0 : 1
               x0,x1 = x1,x0
           end
           return x0
       end
stable (generic function with 1 method)

julia> @code_warntype stable(fill(1,10),fill(1,10))
MethodInstance for stable(::Vector{Int64}, ::Vector{Int64})
  from stable(rv, x0) @ Main REPL[21]:1
Arguments
  #self#::Core.Const(stable)
  rv::Vector{Int64}
  x0@_3::Vector{Int64}
Locals
  @_4::Union{Nothing, Tuple{Int64, Int64}}
  x1::Vector{Int64}
  t::Int64
  p::Int64
  #5::var"#5#6"{Vector{Int64}}
  x0@_9::Vector{Int64}
  x0@_10::Vector{Int64}
  @_11::Int64
Body::Vector{Int64}
1 ─       (x0@_10 = x0@_3)
β”‚         (x1 = Main.copy(x0@_10))
β”‚   %3  = (1:10)::Core.Const(1:10)
β”‚         (@_4 = Base.iterate(%3))
β”‚   %5  = (@_4::Core.Const((1, 1)) === nothing)::Core.Const(false)
β”‚   %6  = Base.not_int(%5)::Core.Const(true)
└──       goto #7 if not %6
2 β”„ %8  = @_4::Tuple{Int64, Int64}
β”‚         (t = Core.getfield(%8, 1))
β”‚   %10 = Core.getfield(%8, 2)::Int64
β”‚   %11 = x0@_10::Vector{Int64}
β”‚         (x0@_9 = %11)
β”‚   %13 = Main.:(var"#5#6")::Core.Const(var"#5#6")
β”‚   %14 = Core.typeof(x0@_9)::Core.Const(Vector{Int64})
β”‚   %15 = Core.apply_type(%13, %14)::Core.Const(var"#5#6"{Vector{Int64}})
β”‚         (#5 = %new(%15, x0@_9))
β”‚   %17 = #5::var"#5#6"{Vector{Int64}}
β”‚   %18 = Base.Generator(%17, rv)::Base.Generator{Vector{Int64}, var"#5#6"{Vector{Int64}}}
β”‚   %19 = (:init,)::Core.Const((:init,))
β”‚   %20 = Core.apply_type(Core.NamedTuple, %19)::Core.Const(NamedTuple{(:init,)})
β”‚   %21 = Core.tuple(1)::Core.Const((1,))
β”‚   %22 = (%20)(%21)::Core.Const((init = 1,))
β”‚         (p = Core.kwcall(%22, Main.prod, %18))
β”‚   %24 = (p == 1)::Bool
└──       goto #4 if not %24
3 ─       (@_11 = 0)
└──       goto #5
4 ─       (@_11 = 1)
5 β”„ %29 = @_11::Int64
β”‚         Base.setindex!(x1, %29, 1)
β”‚   %31 = x1::Vector{Int64}
β”‚   %32 = x0@_10::Vector{Int64}
β”‚         (x0@_10 = %31)
β”‚         (x1 = %32)
β”‚         (@_4 = Base.iterate(%3, %10))
β”‚   %36 = (@_4 === nothing)::Bool
β”‚   %37 = Base.not_int(%36)::Bool
└──       goto #7 if not %37
6 ─       goto #2
7 β”„       return x0@_10
7 Likes

I see… Thanks a lot!