How does constant propagation work?

Let’s say I start with the following:

julia> function f(x, y) 
         if y > 10
           x + 1
         else
           x * 2
         end
       end
f (generic function with 1 method)

julia> @code_warntype f(2, 20)
Body::Int64
1 ─ %1 = (Base.slt_int)(10, y)::Bool
└──      goto #3 if not %1
2 ─ %3 = (Base.add_int)(x, 1)::Int64
└──      return %3
3 ─ %5 = (Base.mul_int)(x, 2)::Int64
└──      return %5

Then, I realize that the compiler can optimize away a branch if it knows that it always choose the other branch:

julia> g() = f(2,3)
g (generic function with 2 methods)

julia> @code_warntype g()
Body::Int64
1 ─     return 4

That’s great, but if I define a global constant then why wouldn’t it optimize in this case?

julia> const cy = 3
3

julia> g(y) = f(2, y)
g (generic function with 2 methods)

julia> @code_warntype g(cy)
Body::Int64
1 ─ %1 = (Base.slt_int)(10, y)::Bool
└──      goto #3 if not %1
2 ─      goto #4
3 ─      goto #4
4 ┄ %5 = φ (#2 => 3, #3 => 4)::Int64
└──      return %5

You are not using the macro in a way that allows constant propagation, cf:

julia> h() = g(cy)
h (generic function with 1 method)

julia> @code_warntype h()
Body::Int64
1 ─     return 4
4 Likes

Thanks, but my question is why the compiler cannot figure it out and optimize it away? Am I missing something?

It has to do with how @code_warntype (and all the @code_* introspection functionality) works — it doesn’t care what the values of the arguments are that you pass in. It simply takes their types, looks up the relevant method, and then gets the code for that method.

Thus, you need a helper function like Kristoffer’s h() above to actually put the constant values in a place where Julia can see them.

3 Likes

Thanks for the explanation. Does that mean the optimization would actually kick in when I pass constants, and I just can’t see it properly because of the way that I use @code_warntype?

Yes. Maybe not if the constants comes from global scope, not sure.

1 Like