Unexpected [red] Any from code_warntype

Hi, new to Julia, still working stuff out. I get the same feedback from code_warntype in a larger code base, and can reproduce it with the below.

I executed the below via: ~/julia/julia --color=yes -O0 -g 2 test.jl

I understood it was a mistake to use code_warntype with optimised. The output of my code (see below at the base of the code) is next where the first two Any’s are in red (I can’t change the colour in the post):

Body::Any
18 1 ─ %1 = (Base.getfield)(x, f)::Any                                                                                   │
   └──      return %1                                                                                                    │
CodeInfo(
18 1 ─ %1 = (Base.getfield)(x, f)::Any                                                                                   │
   └──      return %1                                                                                                    │
) => Any
Int64

My question simply is should I expect output in red and the type to be Any? I had thought that with the code as below the compiler knew at compile time the types. In my larger code I also get a similar report from code_warntype and was wondered whether as I’m new to Julia I’ve miss-understood the type system and not provided enough type info for the dispatch to be fast and type stable. Could anyone tell me if I should be concerned with this output (red=type unstable was my understanding)?

Interestingly if you remove MMatrix the Any below goes to a Union, still in red however (again, can’t reproduce that here in the post):

Body::Union{Bool, Int64, Point2D, String}
18 1 ─ %1 = (Base.getfield)(x, f)::Union{Bool, Int64, Point2D, String}                                                   │
   └──      return %1                                                                                                    │

I suppose my central question is that as a newby I’ve been writing units tests, basically printing type information with things like code_warntype of relatively complex collections of composite types and looking to assure the compiler knows all the types and is type stable: I don’t know if I should be ignoring this and it’s just to be expected for a type of this nature, or I should be looking at it and fixing something…help really appreciated.

General comments also welcome if you spot something unrelated to my central question.
Thanks,
Andy

module test
using InteractiveUtils: @code_warntype, @code_typed, @code_lowered
using StaticArrays

struct Point2D <: FieldVector{2, Float64}
    x::Float64
    y::Float64
end

Zero(p::Point2D) = Point2D((0.0, 0.0))

# {P} here because it could be a Point1D/Point2D/Point3D
mutable struct Vert{P}
    index::Int64
    pos::P
    previousPos::P
    meta::String
    active::Bool
    lhs::MMatrix{2, 2, Float64}
    # many other fields
    Vert{P}(i::Int64, p::P) where {P} = new(i, p, Zero(p), "", true, zeros(MMatrix{2, 2, Float64}))
end

const Vert2D = Vert{Point2D}
v = Vert2D(1, Point2D(1.0, 2.0))

const Verts = Vector{Vert2D}
verts = Verts()
push!(verts, v)

@code_warntype verts[end].index
println(@code_typed verts[end].index)
println(typeof(verts[end].index))
end 
1 Like

This seems to be at least in part related to being run in the global scope. If you wrap the expression in a function:

       f(x) = x[end].index
       @code_warntype f(verts)
       println(@code_typed f(verts))

you get

Body::Int64
31 1 ─ %1 = (Base.arraysize)(x, 1)::Int64               │╻╷╷╷╷    lastindex
   │   %2 = (Base.slt_int)(%1, 0)::Bool                 ││╻╷╷╷     eachindex
   │   %3 = (Base.ifelse)(%2, 0, %1)::Int64             │││┃│││││   axes1
   │   %4 = (Base.arrayref)(true, x, %3)::Main.test.Vert{Main.test.Point2D}
   │   %5 = (Base.getfield)(%4, :index)::Int64          │╻        getproperty
   └──      return %5                                   │        
CodeInfo(
31 1 ─ %1 = (Base.arraysize)(x, 1)::Int64               │╻╷╷╷╷    lastindex
   │   %2 = (Base.slt_int)(%1, 0)::Bool                 ││╻╷╷╷     eachindex
   │   %3 = (Base.ifelse)(%2, 0, %1)::Int64             │││┃│││││   axes1
   │   %4 = (Base.arrayref)(true, x, %3)::Vert{Point2D} │╻        getindex
   │   %5 = (Base.getfield)(%4, :index)::Int64          │╻        getproperty
   └──      return %5                                   │        
) => Int64
Int64
Main.test
1 Like

Ok thanks. So that’s a miss-understanding by me. I had read about this type of trick when using the REPL in the global namespace but I had assumed that my code called within the module albeit outwith a function meant that it’s within the module name space and this didn’t matter. Do you mean any name space outwith a function this would be a problem? Could you help me understand why this makes a difference and what is happening/going wrong?

Thanks,
Andy

Modules create a new global scope, other ways to create scope produce local scope such as let blocks, for loops, functions,… ect. produce local scopes.

A little more investigation shows that this however appears not the reason you were getting the Any

julia> function f()
         @code_warntype verts[end].index
       end
f (generic function with 2 methods)

julia> f()
Body::Any
18 1 ─ %1 = (Base.getfield)(x, f)::Any                                      │
   └──      return %1    

This seems to meant that it is more related to using @code_warntype on an expression rather then a function.

1 Like

Thank you. Could I ask, are you saying that this is new information to you? As in, it’s usage on an expression directly is given unexpected output, or are you saying that this is known behaviour and that I should avoid doing so? Clearly the results of the two are dramatically different and lead to different conclusions about the code…
Thanks,
Andy

It is new information to me, I have never used @code_warntype on such an expression before, I normally use the function code_warntype rather than the macro, for example:

julia> code_warntype(x->2x,(Int,))
Body::Int64
1 1 ─ %1 = (Base.mul_int)(2, x)::Int64                                                        │╻ *
  └──      return %1   

The Any here is totally expected. verts is still a global variable and could be of any type (which might even change over time). How should the compiler deduce the return type? Hence the Any.

1 Like

Thanks. Not sure I fully grasp this.

Makes me wonder more generally, if this is the case for emitted code from the compiler then that implies that any code within a global namespace, say in a module as above, is less performant by the same argument, no? So not in a function is by definition working from your logic much slower? Or at least type unstable. Is this a general truism to work to?

In principle, yes. This is also mentioned in the Performance Tips. I highly suggest you read them - in particular the first two.

In short: if a piece of code operates on non-constant global variables, and code in toplevel namespaces such as the REPL typically does, the compiler can’t be sure of the variables type and hence can’t specialize on the same. Hence the code is slower.

Thank you. I did read them. So to be clear I should treat code within a module but outwith a function as effective global namespace? I wasn’t aware of that. I’m not really using the REPL so I’m more interested in a module based group of files. Thanks

The macro uses the function internally as well:

julia> @macroexpand @code_warntype 3+3
:((InteractiveUtils.code_warntype)(+, (Base.typesof)(3, 3)))

So they give the same output

julia> @code_warntype 3+3
Body::Int64
53 1 ─ %1 = (Base.add_int)(x, y)::Int64                                                                                                                                  │
   └──      return %1                                                                                                                                                    │

julia> code_warntype(+, (Int, Int))
Body::Int64
53 1 ─ %1 = (Base.add_int)(x, y)::Int64                                                                                                                                  │
   └──      return %1

Yes. It is a global namespace.

Note, however, that functions aren’t the only constructs that introduce a local scope. Other examples would for example be begin ... else or let ... end blocks.

Read, for example, this section of the julia docs.

since:

julia> code_warntype(InteractiveUtils.getproperty, (test.Vert{Main.test.Point2D},Symbol))
Body::Any
18 1 ─ %1 = (Base.getfield)(x, f)::Any                                                           │
   └──      return %1  

and since replacing the type of lhs with other types not otherwise found in the definition such as Float32 gives the same result. This appears to be related to a lack of constant propagation. In a function (if verts is made const) it can narrow down the number of types that getproperty can return, however just from the type information this cannot be determined. It appears that in this case rather then return a union of five types it is returning Any.

while verts is a non-const in the global scope this optimization cannot be made. Additionally @code_warntype cannot directly tell if a type is stable unless it is wrapped in a function.

julia> const verts2 = verts
1-element Array{Vert{Point2D},1}:
 Vert{Point2D}(1, [1.0, 2.0], [0.0, 0.0], "", true, [0.0 0.0; 0.0 0.0])

julia> f(x) = x[end].index
f (generic function with 1 method)

julia> f2() = verts[end].index
f2 (generic function with 1 method)

julia> f3() = verts2[end].index
f3 (generic function with 1 method)

julia> f4(x,y) = getproperty(x[end],y)
f4 (generic function with 1 method)
 
julia> @code_warntype f(verts) #getproperty can use const propogation to determine type of return
Body::Int64
1 1 ─ %1 = (Base.arraysize)(x, 1)::Int64                                             │╻╷╷╷╷    lastindex
  │   %2 = (Base.slt_int)(%1, 0)::Bool                                               ││╻╷╷╷     eachindex
  │   %3 = (Base.ifelse)(%2, 0, %1)::Int64                                           │││┃│││││   axes1
  │   %4 = (Base.arrayref)(true, x, %3)::Vert{Point2D}                               │╻        getindex
  │   %5 = (Base.getfield)(%4, :index)::Int64                                        │╻        getproperty
  └──      return %5                                                                 │        

julia> @code_warntype f2() #verts type unstable return type undecidable
Body::Any
1 1 ─ %1 = (Base.lastindex)(Main.verts)::Any                                                             │
  │   %2 = (Base.getindex)(Main.verts, %1)::Any                                                          │
  │   %3 = (Base.getproperty)(%2, :index)::Any                                                           │
  └──      return %3                                                                                     │

julia> @code_warntype f3() #vert2s type stable getproperty can be const propogated
Body::Int64
1 1 ─ %1 = Main.verts2::Core.Compiler.Const(Vert{Point2D}[Vert{Point2D}(1, [1.0, 2.0], [0.0, 0.0], "", true, [0.0 0.0; 0.0 0.0])], false)
  │   %2 = (Base.arraysize)(%1, 1)::Int64                                            │╻╷╷╷     lastindex
  │   %3 = (Base.slt_int)(%2, 0)::Bool                                               ││╻╷╷╷     eachindex
  │   %4 = (Base.ifelse)(%3, 0, %2)::Int64                                           │││┃│││││   axes1
  │   %5 = Main.verts2::Core.Compiler.Const(Vert{Point2D}[Vert{Point2D}(1, [1.0, 2.0], [0.0, 0.0], "", true, [0.0 0.0; 0.0 0.0])], false)
  │   %6 = (Base.arrayref)(true, %5, %4)::Vert{Point2D}                              │╻        getindex
  │   %7 = (Base.getfield)(%6, :index)::Int64                                        │╻        getproperty
  └──      return %7                                                                 │        

julia> @code_warntype f4(verts,:index) #const propagation of getproperty cannot be used
Body::Any
1 1 ─ %1 = (Base.arraysize)(x, 1)::Int64                                             │╻╷╷╷╷    lastindex
  │   %2 = (Base.slt_int)(%1, 0)::Bool                                               ││╻╷╷╷     eachindex
  │   %3 = (Base.ifelse)(%2, 0, %1)::Int64                                           │││┃│││││   axes1
  │   %4 = (Base.arrayref)(true, x, %3)::Vert{Point2D}                               │╻        getindex
  │   %5 = (Base.getfield)(%4, y)::Any                                               │╻        getproperty
  └──      return %5                                                                 

julia> @code_warntype verts2[end].index #const propogation cannot be used for getproperty
Body::Any
18 1 ─ %1 = (Base.getfield)(x, f)::Any                                                                   │
   └──      return %1                                                                                    │

julia> push!(verts2,v) #const refers to type stability not immutability for mutable structs
2-element Array{Vert{Point2D},1}:
 Vert{Point2D}(1, [1.0, 2.0], [0.0, 0.0], "", true, [0.0 0.0; 0.0 0.0])
 Vert{Point2D}(1, [1.0, 2.0], [0.0, 0.0], "", true, [0.0 0.0; 0.0 0.0])                                           │

This is the reason for the Any here.

Using @code_warntype like this:

julia> struct Foo
       x::Int
       y::Float64
       end

julia> f = Foo(1, 2.0)
Foo(1, 2.0)

julia> @code_warntype f.x
Body::Union{Float64, Int64}
18 1 ─ %1 = (Base.getfield)(x, f)::Union{Float64, Int64}
   └──      return %1 

Is exactly like writing

julia> code_warntype(getproperty, Tuple{Foo, Symbol})
Body::Union{Float64, Int64}
18 1 ─ %1 = (Base.getfield)(x, f)::Union{Float64, Int64} 
   └──      return %1  

There is no way from only the type information provided Julia will be able to know if you are going to access the x and the y field. The solution is indeed to wrap in a function and call @code_warntype from outside the function:

julia> g(f) = f.x
g (generic function with 1 method)

julia> @code_warntype g(f)
Body::Int64
1 1 ─ %1 = (Base.getfield)(f, :x)::Int64 
  └──      return %1  

The relevant PR to track for enabling constant propagation in the code_ tools is: Extend code_typed to be able to debug constant prop by Keno · Pull Request #29261 · JuliaLang/julia · GitHub

2 Likes