I have observed a surprising behaviour of map
with closures. It seems that type instabilities appear when the function called by map
overwrites a variable that belongs to an outer scope (only changing its value while keeping the same type). Here is a MWE:
function map_bad(x)
r = rand(typeof(first(x)))
c = r
map(x) do xi
r = rand(typeof(xi))
xi + c + r
end
end
function map_good(x)
r = rand(typeof(first(x)))
c = r
map(x) do xi
ri = rand(typeof(xi))
xi + c + ri
end
end
using BenchmarkTools
x = rand(10)
@btime map_bad($x); # 3.193 Ξs (44 allocations: 1.00 KiB)
@btime map_good($x); # 40.237 ns (1 allocation: 144 bytes)
I am quite surprised by the difference in execution time and memory allocations. I inspected both functions with @code_warntype
:
MethodInstance for map_bad(::Vector{Float64})
from map_bad(x) @ Main REPL[2]:1
Arguments
#self#::Core.Const(map_bad)
x::Vector{Float64}
Locals
#5::var"#5#6"
c::Any
r@_5::Core.Box
r@_6::Union{}
Body::Vector
1 â Core.NewvarNode(:(#5))
â Core.NewvarNode(:(c))
â (r@_5 = Core.Box())
â %4 = Main.first(x)::Float64
â %5 = Main.typeof(%4)::Core.Const(Float64)
â %6 = Main.rand(%5)::Float64
â Core.setfield!(r@_5, :contents, %6)
â %8 = Core.isdefined(r@_5, :contents)::Bool
âââ goto #3 if not %8
2 â goto #4
3 â Core.NewvarNode(:(r@_6))
âââ r@_6
4 â (c = Core.getfield(r@_5, :contents))
â %14 = Main.:(var"#5#6")::Core.Const(var"#5#6")
â %15 = Core.typeof(c)::DataType
â %16 = Core.apply_type(%14, %15)::Type{var"#5#6"{_A}} where _A
â %17 = c::Any
â (#5 = %new(%16, %17, r@_5))
â %19 = #5::var"#5#6"
â %20 = Main.map(%19, x)::Vector
âââ return %20
MethodInstance for map_good(::Vector{Float64})
from map_good(x) @ Main REPL[3]:1
Arguments
#self#::Core.Const(map_good)
x::Vector{Float64}
Locals
#7::var"#7#8"{Float64}
c::Float64
r::Float64
Body::Vector{Float64}
1 â %1 = Main.first(x)::Float64
â %2 = Main.typeof(%1)::Core.Const(Float64)
â (r = Main.rand(%2))
â (c = r)
â %5 = Main.:(var"#7#8")::Core.Const(var"#7#8")
â %6 = Core.typeof(c)::Core.Const(Float64)
â %7 = Core.apply_type(%5, %6)::Core.Const(var"#7#8"{Float64})
â (#7 = %new(%7, c))
â %9 = #7::var"#7#8"{Float64}
â %10 = Main.map(%9, x)::Vector{Float64}
âââ return %10
In the case of map_bad
, I do not understand why r
is created this way. I am also surprised that the type of c
cannot be inferred at compile time. Does anyone have an idea of what is happening?