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?