Search code examples
juliatemporarytype-stability

Julia @code_warntype reveals hidden temporary variable #temp#


I ran a type-stability check on my code recently. When I call @code_warntype on it, I get the following output:

Variables:
  #unused#::IHT.#kw##L0_reg
  #temp#@_2::Array{Any,1}
  ::IHT.#L0_reg
  x::Array{Float64,2}
  y::Array{Float64,1}
  k::Int64
  #temp#@_7::Bool
  #temp#@_8::Bool
  max_iter::Int64
  max_step::Int64
  quiet::Bool
  v::IHT.IHTVariables{Float64,Array{Float64,1}}
  tol::Float64
  #temp#@_14::Int64
  #temp#@_15::Int64
  #temp#@_16::Int64
  #temp#@_17::Any
  ###...###
  #temp#@_17::Any = (Core.arrayref(#temp#@_2::Array{Any,1},#temp#@_16::Int64)::Any
  ###...###
  v::IHT.IHTVariables{Float64,Array{Float64,1}} = (Core.typeassert)((Core.arrayref)(#temp#@_2::Array{Any,1},(Base.box)(Int64,(Base.add_int)(#temp#@_16::Int64,1)))::Any,IHT.IHTVariables{Float64,Array{Float64,1}})::IHT.IHTVariables{Float64,Array{Float64,1}}

Minimal working example, using my IHT.jl package:

Pkg.clone("https://github.com/klkeys/IHT.jl")
n = 100; p = 250; k = 2;
x = randn(n,p)
b = zeros(p); b[1:k] = randn(k); shuffle!(b)
y = x*b + randn(n)
@code_warntype L0_reg(x, y, k, quiet=true)

It would seem like the compiler is using #temp# to read the arguments to the function L0_reg. The function arguments are completely specified. From where does this evil little #temp#@_2 variable arise? Am I able to tell the compiler what its type is? (hopefully not Array{Any,1}...)


Solution

  • You can use @code_lowered to view where the #temp# variables are coming from:

    julia> @code_lowered L0_reg(x, y, k, quiet=true)
    LambdaInfo template for (::IHT.#kw##L0_reg){T<:Union{Float32,Float64}}(::Array{Any,1}, ::IHT.#L0_reg, x::DenseArray{T,2}, y::DenseArray{T,1}, k::Int64)
    :(begin 
            NewvarNode(:(temp))
            NewvarNode(:(tol))
            #temp#@_7 = true
            #temp#@_8 = true
            max_iter = 100
            max_step = 50
            quiet = true
            SSAValue(0) = (IHT.colon)(1,(Base.length)(#temp#@_2) >> 1)
            #temp#@_14 = (Base.start)(SSAValue(0))
            10: 
            unless !((Base.done)(SSAValue(0),#temp#@_14)) goto 42
            SSAValue(1) = (Base.next)(SSAValue(0),#temp#@_14)
            #temp#@_15 = (Core.getfield)(SSAValue(1),1)
            #temp#@_14 = (Core.getfield)(SSAValue(1),2)
            #temp#@_16 = #temp#@_15 * 2 - 1
            #temp#@_17 = (Core.arrayref)(#temp#@_2,#temp#@_16)
            unless #temp#@_17 === :quiet goto 20
            quiet = (Core.typeassert)((Core.arrayref)(#temp#@_2,#temp#@_16 + 1),IHT.Bool)
            goto 40
            20: 
            unless #temp#@_17 === :max_step goto 24
            max_step = (Core.typeassert)((Core.arrayref)(#temp#@_2,#temp#@_16 + 1),IHT.Int)
            goto 40
            24: 
            unless #temp#@_17 === :max_iter goto 28
            max_iter = (Core.typeassert)((Core.arrayref)(#temp#@_2,#temp#@_16 + 1),IHT.Int)
            goto 40
            28: 
            unless #temp#@_17 === :tol goto 33
            tol = (Core.typeassert)((Core.arrayref)(#temp#@_2,#temp#@_16 + 1),IHT.Float)
            #temp#@_8 = false
            goto 40
            33: 
            unless #temp#@_17 === :temp goto 38
            temp = (Core.typeassert)((Core.arrayref)(#temp#@_2,#temp#@_16 + 1),(Core.apply_type)(IHT.IHTVariables,$(Expr(:static_parameter, 1))))
            #temp#@_7 = false
            goto 40
            38: 
            (Base.kwerr)(#temp#@_2,,x,y,k)
            40: 
            goto 10
            42: 
            unless #temp#@_7 goto 45
            temp = (IHT.IHTVariables)(x,y,k)
            45: 
            unless #temp#@_8 goto 48
            tol = (IHT.convert)($(Expr(:static_parameter, 1)),0.0001)
            48: 
            return (IHT.#L0_reg#75)(temp,tol,max_iter,max_step,quiet,,x,y,k)
        end)
    

    In this case, the temps (in particular #temp#@_2) are coming from the keyword arguments. This is quite normal for keyword arguments.

    julia> f(; x=1) = x
    f (generic function with 1 method)
    
    julia> @code_warntype f(x=1)
    Variables:
      #unused#::#kw##f
      #temp#@_2::Array{Any,1}
      ::#f
      x::Any
      #temp#@_5::Int64
      #temp#@_6::Int64
      #temp#@_7::Int64
      #temp#@_8::Any
    
    Body:
      begin 
          x::Any = 1
          SSAValue(2) = (Base.arraylen)(#temp#@_2::Array{Any,1})::Int64
          SSAValue(3) = (Base.select_value)((Base.sle_int)(0,1)::Bool,(Base.box)(Int64,(Base.ashr_int)(SSAValue(2),(Base.box)(UInt64,1))),(Base.box)(Int64,(Base.shl_int)(SSAValue(2),(Base.box)(UInt64,(Base.box)(Int64,(Base.neg_int)(1))))))::Int64
          SSAValue(4) = (Base.select_value)((Base.sle_int)(1,SSAValue(3))::Bool,SSAValue(3),(Base.box)(Int64,(Base.sub_int)(1,1)))::Int64
          #temp#@_5::Int64 = 1
          6: 
          unless (Base.box)(Base.Bool,(Base.not_int)((#temp#@_5::Int64 === (Base.box)(Int64,(Base.add_int)(SSAValue(4),1)))::Bool)) goto 21
          SSAValue(5) = #temp#@_5::Int64
          SSAValue(6) = (Base.box)(Int64,(Base.add_int)(#temp#@_5::Int64,1))
          #temp#@_6::Int64 = SSAValue(5)
          #temp#@_5::Int64 = SSAValue(6)
          #temp#@_7::Int64 = (Base.box)(Int64,(Base.sub_int)((Base.box)(Int64,(Base.mul_int)(#temp#@_6::Int64,2)),1))
          #temp#@_8::Any = (Core.arrayref)(#temp#@_2::Array{Any,1},#temp#@_7::Int64)::Any
          unless (#temp#@_8::Any === :x)::Bool goto 17
          x::Any = (Core.arrayref)(#temp#@_2::Array{Any,1},(Base.box)(Int64,(Base.add_int)(#temp#@_7::Int64,1)))::Any
          goto 19
          17: 
          (Base.throw)($(Expr(:new, :(Base.MethodError), :((Core.getfield)((Core.getfield)((Core.getfield)(#f,:name)::TypeName,:mt),:kwsorter)), :((Core.tuple)(#temp#@_2,)::Tuple{Array{Any,1},#f}))))::Union{}
          19: 
          goto 6
          21: 
          return x::Any
      end::Any
    

    Keyword arguments are known to have callsite overhead that can be somewhat worked around by declaring types. Note that unless your function does very little, it's unlikely the sorting of keyword arguments is actually a huge bottleneck (despite the nasty @code_warntype output).

    When you do @code_warntype on a keyword argument call, you're actually viewing the type instabilities of the keyword argument sorter, an autogenerated wrapper around the real function. As you can see, the code ends up calling (IHT.#L0_reg#75)(temp,tol,max_iter,max_step,quiet,,x,y,k), which is a plain function that takes positional arguments. So the output of @code_warntype is almost useless in this case.