Composing a constructor causes tons of allocations

I’m trying to build a pipeline of transformations and noticed this weird behavior when I plugged a Point2f0 constructor into the pipeline. I’ve removed the rest of the test pipeline and replaced it with the identity function to create the MWE.

using GeometryTypes

f_bad = Point2f0 ∘ identity
f_good = (x -> Point2f0(x)) ∘ identity

ps = rand(Point2{Float64}, 1_000_000)
julia> @btime f_bad.($ps)
102.237 ms (2999498 allocations: 68.66 MiB)
julia> @btime f_good.($ps)
1.304 ms (5 allocations: 7.63 MiB)

Can anybody help me to understand the cause?

2 Likes

That’s interesting–it looks like the closure that is built by the \circ operator ends up capturing f (the first function) as a field of the abstract type DataType:

julia> @code_warntype f_bad(ps[1])
Variables
  #self#::Base.var"#56#57"{DataType,typeof(identity)}
  x::Tuple{Point{2,Float64}}

Body::Any
1 ─ %1 = Core.getfield(#self#, :f)::DataType
│   %2 = Core.getfield(#self#, :g)::Core.Compiler.Const(identity, false)
│   %3 = Core._apply(%2, x)::Point{2,Float64}
│   %4 = (%1)(%3)::Any
└──      return %4

and that makes the result of the composed function un-inferrable.

We can reproduce the issue without using \circ like so:

julia> function wrap(t)
         x -> t(x)
       end
wrap (generic function with 1 method)

julia> wrapped_point = wrap(Point2f0)
#14 (generic function with 1 method)

julia> @code_warntype wrapped_point(ps[1])
Variables
  #self#::var"#14#15"{DataType}
  x::Point{2,Float64}

Body::Any
1 ─ %1 = Core.getfield(#self#, :t)::DataType
│   %2 = (%1)(x)::Any
└──      return %2

Seems like it would be worth opening an issue at Issues · JuliaLang/julia · GitHub , since this seems like a general issue with the way types are captured by closures.

1 Like

There is:

https://github.com/JuliaLang/julia/issues/23618

1 Like