`code_warntype` understanding

Here is a code that puzzles me. How can it be?

@code_warntype model(2.1,0.3,0.3; pars=[1,1,1.0]) # fine, body::Complex{Float64}
#
x(a,b,c) = model(a,b,c; pars=[1,1,1.0])
@code_warntype x(2.1,0.3,0.3) # body::Any

In general, should I care? the function x is passed to Cuba.curhe for multidimensional integration.
Do you think I should try to fix this ::Any?

Thanks

1 Like

I’m very confused with the type inference.

Here is another example

const testLM = [(L=1, M=1), (L=3,M=1)]
const testA = rand(Complex{Float64},2)
#
@code_warntype Psi(3,1,0.3,0.3) # ::Float64
recamp(cosθ,ϕ,amps,LMs) = sum(a*Psi(L,M,cosθ,ϕ) for (a, (L, M)) in zip(amps,LMs))
@code_warntype recamp(0.3,0.3,testA,testLM) # ::Any

but

function test()
    return recamp(0.3,0.3,testA,testLM)
end
@code_warntype test() # Body::Float64
1 Like

For some reason, the original case behaviors differently

function test()
    return x(2.1,0.3,0.3)
end
@code_warntype test() # Body::Any

:confused:

What is Psi or model. What is the output of @code_warntype. It’s impossible to tell without more complete information. (edit: and as a guess model, Psi and x may not be constant)

6 Likes

Sure, it needs some effort to isolate all model calls in a simple code to be posted.
Could you tell me please if it looks to you as awkward, weird and counterintuitive as it does for me?

One way this can show up is via constant propagation. Here is a silly function which is obviously type-unstable:

julia> function f(x)
         if x < 1
           return "hello"
         else
           return 1.0
         end
       end
f (generic function with 1 method)

julia> @code_warntype f(1)
Variables
  #self#::Core.Compiler.Const(f, false)
  x::Int64

Body::Union{Float64, String}

However, if we wrap this in another function and fix its argument, then something interesting happens:

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

julia> @code_warntype g()
Variables
  #self#::Core.Compiler.Const(g, false)
  x::Int64

Body::Float64
1 ─      (x = 2)
│   %2 = Main.f(x::Core.Compiler.Const(2, false))::Core.Compiler.Const(1.0, false)
└──      return %2

When compiling g(), the compiler is able to prove that x = 2 and therefore only the else clause in f(x) will occur, so it knows that g() returns a Float64. In fact, it even knows that g() returns 1.0 (note the Const(1.0, ...)).

@code_warntype f(1) doesn’t show you this because @code_warntype actually only uses the types of the input provided:

julia> @macroexpand @code_warntype f(1)
:(InteractiveUtils.code_warntype(f, (Base.typesof)(1)))

Note that typeof(1) which is simply Int. That means that @code_warntype can’t show you the results of constant propagation in the expression you provide it.

However, in the case of @code_warntype g(), the constant x = 2 is already baked into the definition of g(), so the result of that constant propagation can be shown.

1 Like

No. This can definately happen especially if my guess is true.

No that doesn’t matter at all.

model , Psi and x may not be constant

Minimal example reproducing OP’s problem:

julia> struct Model
           x::Float64
       end

julia> (m::Model)(y) = m.x * y

julia> model = Model(3.5) # model is a non-constant global
Model(3.5)

julia> @code_warntype model(2)
Variables
  m::Model
  y::Int64

Body::Float64
1 ─ %1 = Base.getproperty(m, :x)::Float64
│   %2 = (%1 * y)::Float64
└──      return %2

julia> modelcall() = model(2) # using non-constant global "model" within a function definition
modelcall (generic function with 1 method)

julia> @code_warntype modelcall()
Variables
  #self#::Core.Const(modelcall)

Body::Any
1 ─ %1 = Main.model(2)::Any
└──      return %1

Well, that was rude and unhelpful.

Yes this is the cause as I said.
I thought that was from OP.
I guess it’s still uncertain if this is the actual cause.

Errr, well, but it is simply true that constant propagation doesn’t matter at all here. Please read the original post more carefully to see how the comparison you made was the wrong way around. It should not be unhelpful since I was hoping that the difference is relatively easy to see.

Yes. The OP hadn’t posted a minimal example yet, so I commented with an example illustrating the issue. I think new users can miss the fact that just because something looks like a function, doesn’t mean it is a constant.

A simpler example would have been

model = x -> 3x
@code_warntype model(6)
modelcall() = model(7)
@code_warntype modelcall()

but I figured @misha_mikhasenko’s example may look more similar to the struct definition with an added call method, and seeing the example would help confirm/deny that this is what is going on.

1 Like

FWIW, I have real code for data fitting more similar to

gen_model(p0) = (x, p) -> f(x, p, p0)

and I either assign the result to a const, or use the result inline, or simply in most cases don’t really care about the top level dispatch = = …

1 Like

Offtopic, but maybe important: @rdeits is probably seeing it quite easily, but I do not so easily and OP maybe neither. And even after a while I think I found out, but I am still unsure, if I am not thinking wrong. So, I don’t know who you want to address, but for me your hopes aren’t fulfilled :wink:

2 Likes

thanks to everyone who commented.

Here is the first case, where ::Any desapper once you put the call to the function

module MyTest
    using PartialWaveFunctions
    Psi(L,M,cosθ,ϕ) = sqrt((2L+1)/(2π))*wignerd(L,M,0,cosθ)*sin(M*ϕ)
    recamp(cosθ,ϕ,amps,LMs) = sum(a*Psi(L,M,cosθ,ϕ) for (a, (L, M)) in zip(amps,LMs))
    export recamp, Psi
end
# 
using .MyTest

const testLM = [(L=1, M=1), (L=3,M=1)]
const testA = rand(Complex{Float64},2)
#
@code_warntype Psi(3,1,0.3,0.3) # ::Float64
recamp(0.3,0.3,testA,testLM)
@code_warntype recamp(0.3,0.3,testA,testLM) # ::Any
#
function test()
    return recamp(0.3,0.3,testA,testLM)
end
@code_warntype test() # Body::Float64

PartialWaveFunctions is my package (registered), so it might have something to do with it.
If I replace PartialWaveFunctions.wignerd(L,M,0,cosθ) with SpecialFunctions.gamma(L*cosθ) (just for a check), the problem with ::Any does not show up.

Here is the second example,


module MyTest2
    using Parameters
    function build_model(list_of_settings)
        function model(x; pars)
            return sum(p*modelA(x; settings=s)
                for (p,s) in zip(pars, list_of_settings))
        end
        return model
    end
    function modelA(x; settings)
        @unpack N, k, p = settings
        value = sum((k*x)^i*cis(i*p*x) for i in 1:N)
        return value
    end

    export build_model
end

using .MyTest2

const list_of_settings = [(N=3,k=0.1,p=0.3),
                    (N=5,k=0.5,p=0.3),
                    (N=7,k=0.2,p=0.3)]
# 
model = build_model(list_of_settings)
# 
model(2.2; pars=[1,1,1.0])
@code_warntype model(2.2; settings=[1,1,1.0]) # fine, body::Complex{Float64}
#
M(x) = model(x; pars=[1,1,1.0])
@code_warntype M(2.2) # body::Any

function test()
    return M(2.2)
end
@code_warntype test() # Body::Any

Here wrapping to a test function does not help.

Indeed, the code from @Elrod would be the simplest example of the same behavior

model = x -> 3x
@code_warntype model(6)  # Body::Int64
modelcall() = model(7)
@code_warntype modelcall()  # Body::Any
#
function test()
    modelcall()
end
@code_warntype test() # Body::Any

My question is should I care about this Any in performance optimization? Or it is just @code_warntype fooling me and internally everything is working as efficient as possibly be?

that is very helpful to know, thanks.

1 Like

No, @code_warntype is correct here, because model is a non-constant global. You should either declare it as a constant using the const keyword or make it an argument to modelcall.

2 Likes

Hm, not sure anymore that it is true. For Elrod’s example
setting model to const would cure the problem const model = x -> 3x.
For my case it does not help.

Fixed const model = build_model(list_of_settings) in the MWE above

module MyTest2
    using Parameters
    function build_model(list_of_settings)
        function model(x; pars)
            return sum(p*modelA(x; settings=s)
                for (p,s) in zip(pars, list_of_settings))
        end
        return model
    end
    function modelA(x; settings)
        @unpack N, k, p = settings
        value = sum((k*x)^i*cis(i*p*x) for i in 1:N)
        return value
    end

    export build_model
end

using .MyTest2

const list_of_settings = [(N=3,k=0.1,p=0.3),
                    (N=5,k=0.5,p=0.3),
                    (N=7,k=0.2,p=0.3)]
# 
const model = build_model(list_of_settings) # added const
# 
model(2.2; pars=[1,1,1.0])
@code_warntype model(2.2; pars=[1,1,1.0]) # fine, body::Complex{Float64}
#
M(x) = model(x; pars=[1,1,1.0])
@code_warntype M(2.2) # still Body::Any

Type is still not inferred. What do I miss?