Correct way to declare global constant in modules

Hi guys,

I have a module with a lot of global constants. Those are related to physical quantities, like Earth gravitational parameter. These are declared as:

const R0 = 6378137.0

I was analyzing the type-stability of my code and @code_warntype is marking every expression that uses those constants as Core.Box.

This seems to have impact on performance. I know that I can pass those constants as arguments, but it is not advisable because there are many of them.

Any ideas how can I fix this?

1 Like

That shouldn’t happen if you declare them as const. Can you provide a minimal example?

Maybe it is because the radius of the Earth is not really constant? :wink:

Hi @tkoolen,

Sorry, it took a while to create a minimal working code that reproduces the problem. See the following module:

VERSION >= v"0.4.0-dev+6521" && __precompile__()

module TestConst

export test

# Earth Equatorial radius [m].
const R0 = 6378137.0

# Standard gravitational parameter for Earth [m^3/s^2].
const m0 = 3.986004418e14

function test(n::Number, e::Number)
    # Check if the arguments are valid.
    if (n <= 0)
        throw(ArgumentError("The angular velocity must be greater than 0."))
    end

    if !( 0. <= e < 1. )
        throw(ArgumentError("The eccentricity must be within the interval 0 <= e < 1."))
    end

    # Auxiliary variables.
    sqrt_m0 = sqrt(m0)
    sqrt_e2 = sqrt(1-e^2)

    # Auxiliary constant to compute the functions.
    K1 = 3.0*R0^2*sqrt_m0/(4.0*(1-e^2)^2)

    # Declare the functions that must solved for 0.
    f1(a, i) = ne + 2.0*K1*a^(-3.5)*cos(i)
end

end

If I run @code_warntype test(1,1), then I get:

Variables:
  #self# <optimized out>
  n::Int64
  e::Int64
  sqrt_m0::Float64
  sqrt_e2 <optimized out>
  K1::Core.Box
  f1::TestConst.#f1#1
  #temp#::Bool
  fy::Float64
  fx::Float64

What is interesting though is that if I remove the exceptions, then I get:

Variables:
  #self# <optimized out>
  n <optimized out>
  e::Int64
  sqrt_m0::Float64
  sqrt_e2 <optimized out>
  K1::Any
  f1::TestConst.#f1#1{_} where _

Good point! :slight_smile: Should have thought about it earlier!

Guys,

The problem is not related with the global constants, but with the internal K1. This other code reproduces the problem as well:

VERSION >= v"0.4.0-dev+6521" && __precompile__()

module TestConst

export test

function test(n::Number, e::Number)
    # Check if the arguments are valid.
    if (n <= 0)
        throw(ArgumentError("The angular velocity must be greater than 0."))
    end

    # Auxiliary constant to compute the functions.
    K1 = 3.0

    # Declare the functions that must solved for 0.
    f1(a, i) = K1
end

end

Notice that, if I remove the exceptions, then everything is fine:

Variables:
  #self# <optimized out>
  n <optimized out>
  e <optimized out>
  K1 <optimized out>
  f1::TestConst.#f1#1{Float64}

Am I missing something?

No. I have the same problem with constants that are used in functions and have concluded that this is due to the closure bug #15276, the single most annoying remaining issue in Julia (in my opinion). If this and absolutely nothing else were miraculously fixed in 1.0 then the release would be marked a success in my book.

2 Likes

Hi @NickNack,

Yes, it indeed seems a similar problem! I think I will have to wait then :slight_smile:

Thanks!

No, there isn’t a problem here. Read the whole code, not just the header:

Variables:
  #self# <optimized out>
  n::Int64
  e <optimized out>
  K1::Core.Box
  f1::#f1#15

Body:
  begin
      K1::Core.Box = $(Expr(:new, :(Core.Box)))
      NewvarNode(:(f1::#f1#15))
      unless (Base.sle_int)(n::Int64, 0)::Bool goto 6 # lin
e 139:
      (Main.throw)($(Expr(:new, :(Base.ArgumentError), "The
 angular velocity must be greater than 0.")))::Union{}
      6:  # line 143:
      (Core.setfield!)(K1::Core.Box, :contents, 3.0)::Float
64 # line 146:
      f1::#f1#15 = $(Expr(:new, :(Main.#f1#15), :(K1)))
      return f1::#f1#15
  end::#f1#15

Core.Box doesn’t mean that there’s a type instability. It just means the variable gets boxed, here because it might not be needed depending on if there’s an exception or not. But its result is not type-unstable. In fact,

(Core.setfield!)(K1::Core.Box, :contents, 3.0)::Float64 # line 146:

this line says that it knows that the contents will be Float64, so all of the types in the AST are inferred and everything is fine.

4 Likes

Hi @ChrisRackauckas

Sorry, it is a part of a bigger code and I maybe misunderstood the real problem. See this other code:

VERSION >= v"0.4.0-dev+6521" && __precompile__()

module TestConst

export test

# Earth Equatorial radius [m].
const R0 = 6378137.0

# Standard gravitational parameter for Earth [m^3/s^2].
const m0 = 3.986004418e14

# Perturbation terms based on EGM-08 standard gravitational model [1, pp. 1039].
const J2 = 1.08262617385222e-3

# Earth's orbit mean motion [rad/s]
const ne = (360.0/365.2421897)*pi/180/86400

function test(n::Number, e::Number)
    # Check if the arguments are valid.
    if (n <= 0)
        throw(ArgumentError("The angular velocity must be greater than 0."))
    end

    if !( 0. <= e < 1. )
        throw(ArgumentError("The eccentricity must be within the interval 0 <= e < 1."))
    end

    # Auxiliary variables.
    sqrt_m0 = sqrt(m0)
    sqrt_e2 = sqrt(1-e^2)

    # Auxiliary constant to compute the functions.
    K1 = 3.0*R0^2*J2*sqrt_m0/(4.0*(1-e^2)^2)

    # Declare the functions that must solved for 0.
    f1(a, i) = ne + 2.0*K1*a^(-3.5)*cos(i)

    a_k::Float64 = (m0/n^2)^(1/3)
    i_k::Float64 = acos( -ne*a_k^(3.5)/(2*K1) )

    f1(a_k, i_k)
end

end

In this case, it is not correcting guessing the return value:

return $(Expr(:invoke, MethodInstance for (::TestConst.#f1#1)(::Float64, ::Float64), :(f1), SSAValue(1), :(i_k)))
  end::Any

On the other hand, if I remove K1 from the function f(a,i), then it works:

return $(Expr(:invoke, MethodInstance for (::TestConst.#f1#1)(::Float64, ::Float64), :(f1), SSAValue(0), SSAValue(1)))
  end::Float64

NOTE: I know that I can always do K1::Float64, but I want to avoid that.

The code is type-stable in the sense that the output type is inferred correctly, but there’s still a performance issue here: the code in this post allocates with the if statement in place, while it doesn’t without that if statement. I think that @NickNack’s assessment (performance of captured variables in closures · Issue #15276 · JuliaLang/julia · GitHub) is correct, especially given that the let block workaround fixes the performance problem:

Edit: I’m no longer convinced that it’s performance of captured variables in closures · Issue #15276 · JuliaLang/julia · GitHub, as the let block trick doesn’t work.

Should I do something? Comment on this bug perhaps?

I’d give it a minute, maybe someone who knows more about the internals of Julia will stop by this post.

Actually, I didn’t apply the let block trick correctly previously. The following doesn’t exhibit the performance problem:

module TestConst

export test

function test(n::Number, e::Number)
    # Check if the arguments are valid.
    if (n <= 0)
        throw(ArgumentError("The angular velocity must be greater than 0."))
    end

    # Auxiliary constant to compute the functions.
    K1 = 3.0

    # Declare the functions that must solved for 0.
    f1 = let K1 = K1
        (a, i) -> K1
    end
end

end

This isn’t the basic 15276 because a much fancier optimization is required. Naively, the inner function f is visible and could be called throughout the outer test function—before or after K0 is defined. In order to correctly implement that, K0 must be boxed so that it’s state can be either unassigned or assigned. The compiler would have to prove that f cannot be called before K0 defined in order to know that it doesn’t need to do any boxing here. It’s possible but quite tricky. You could try moving the definition of K0 before the error checks or putting the main computation into a separate function body that is called after the checks are done. It’s also worth filing an issue about this situation since we would ideally like to handle it efficiently.

7 Likes

Hi guys,

First of all, thanks for the help. As per @StefanKarpinski advice, I opened a issue in gitbhub: Variable type is not inferred correctly if an `if` statement is added · Issue #26752 · JuliaLang/julia · GitHub

Sorry about the technical description and title of the issue, I really do not have the necessary technical background to do any better :slight_smile:

You are right! If I put K1 before the if, then the problem is gone!

1 Like