First I would make a difference between:
- a bug: incorrect behavior
- performance issue: correct behavior but slow performance
The case we observe here is performance issue in my opinion. In particular, what we discuss is the performance of Julia in its current version (I tested it on 1.8.0 - in future versions the performance of the same code might change).
Let me narrow down the MWE even more, as you do not need two closures to have the undesired behavior:
julia> function market_share()
price() = elast / (elast - 1)
elast = 10
return price()
end
market_share (generic function with 1 method)
julia> @code_warntype market_share()
MethodInstance for market_share()
from market_share() in Main at REPL[1]:1
Arguments
#self#::Core.Const(market_share)
Locals
elast::Core.Box
price::var"#price#1"
Body::Any
1 ─ (elast = Core.Box())
│ (price = %new(Main.:(var"#price#1"), elast))
│ Core.setfield!(elast, :contents, 10)
│ %4 = (price)()::Any
└── return %4
What does it show us? As @aplavin explained in an answer the elast variable is defined after the price closure is created. CURRENT state of the Julia compiler in this case does not do a look-ahead processing of the whole function body to check that elast is guaranteed to be a constant (this might change in the future). Therefore it assumes its type is not known and thus does boxing.
When Julia creates a closure (like price in your examples) then what it does is described here. As you can see a special struct is created and this struct needs to capture the elast variable as its field. Again - this is a situation at current state of the Julia compiler. It might change in the future.
If you move elast in front of price things work as expected:
julia> function market_share2()
elast = 10
price() = elast / (elast - 1)
return price()
end
market_share2 (generic function with 1 method)
julia> @code_warntype market_share2()
MethodInstance for market_share2()
from market_share2() in Main at REPL[3]:1
Arguments
#self#::Core.Const(market_share2)
Locals
price::var"#price#2"{Int64}
elast::Int64
Body::Float64
1 ─ (elast = 10)
│ %2 = Main.:(var"#price#2")::Core.Const(var"#price#2")
│ %3 = Core.typeof(elast::Core.Const(10))::Core.Const(Int64)
│ %4 = Core.apply_type(%2, %3)::Core.Const(var"#price#2"{Int64})
│ (price = %new(%4, elast::Core.Const(10)))
│ %6 = (price::Core.Const(var"#price#2"{Int64}(10)))()::Core.Const(1.1111111111111112)
└── return %6
as current compiler can prove that elast is a constant.
However, any assignment to elast after price definition makes inference currently fail:
julia> function market_share3()
elast = 10
price() = elast / (elast - 1)
elast = 10
return price()
end
market_share3 (generic function with 1 method)
julia> @code_warntype market_share3()
MethodInstance for market_share3()
from market_share3() in Main at REPL[5]:1
Arguments
#self#::Core.Const(market_share3)
Locals
price::var"#price#3"
elast::Core.Box
Body::Any
1 ─ (elast = Core.Box())
│ Core.setfield!(elast, :contents, 10)
│ (price = %new(Main.:(var"#price#3"), elast))
│ Core.setfield!(elast, :contents, 10)
│ %5 = (price)()::Any
└── return %5
The same is if even you have an assignment to elast twice before price is defined:
julia> function market_share4()
elast = 10
elast = 11
price() = elast / (elast - 1)
return price()
end
market_share4 (generic function with 1 method)
julia> @code_warntype market_share4()
MethodInstance for market_share4()
from market_share4() in Main at REPL[3]:1
Arguments
#self#::Core.Const(market_share4)
Locals
price::var"#price#2"
elast::Core.Box
Body::Any
1 ─ (elast = Core.Box())
│ Core.setfield!(elast, :contents, 10)
│ Core.setfield!(elast, :contents, 11)
│ (price = %new(Main.:(var"#price#2"), elast))
│ %5 = (price)()::Any
└── return %5
I hope this helps.