Skip to content

Significant performance regression for types with unions in 1.3.0 #55

@ancapdev

Description

@ancapdev
using BenchmarkTools
using ConstructionBase

struct A
    x::Int
    y::Int
end

struct B
    x::Int
    y::Union{Nothing, Int}
end

function update(x)
    setproperties(x, (; x = x.x + 1))
end

function update2(x::T) where T
    T(x.x + 1, x.y)
end

println("Type without union")
@btime update($(Ref(A(1, 2)))[])
@btime update2($(Ref(A(1, 2)))[])

println("Type with union")
@btime update($(Ref(B(1, 2)))[])
@btime update2($(Ref(B(1, 2)))[])

Results 1.3.0

Type without union
  1.182 ns (0 allocations: 0 bytes)
  1.182 ns (0 allocations: 0 bytes)
Type with union
  381.271 ns (6 allocations: 176 bytes)
  12.597 ns (0 allocations: 0 bytes)

Results 1.2.1

Type without union
  1.182 ns (0 allocations: 0 bytes)
  1.182 ns (0 allocations: 0 bytes)
Type with union
  12.597 ns (0 allocations: 0 bytes)
  12.597 ns (0 allocations: 0 bytes)

This appears to be due to the change to remove the use of generated functions for setproperties. Whereas before it would generate the equivalent of a hand rolled function (update2), now it relies on going through named tuples for its merging logic. The issue being, getproperties is type unstable in the presence of union fields. In this example:

x = B(1, 2)
@code_warntype ConstructionBase.setproperties_object(x, (; x = x.x + 1))

Outputs

MethodInstance for ConstructionBase.setproperties_object(::Main.Foo.B, ::NamedTuple{(:x,), Tuple{Int64}})
  from setproperties_object(obj, patch) in ConstructionBase at /home/christian/.julia/dev/ConstructionBase/src/ConstructionBase.jl:145
Arguments
  #self#::Core.Const(ConstructionBase.setproperties_object)
  obj::Main.Foo.B
  patch::NamedTuple{(:x,), Tuple{Int64}}
Locals
  nt_new::NamedTuple{(:x, :y)}
  nt::NamedTuple{(:x, :y), _A} where _A<:Tuple{Int64, Union{Nothing, Int64}}
Body::Main.Foo.B
1nothing
│        (nt = ConstructionBase.getproperties(obj))
│        (nt_new = ConstructionBase.merge(nt, patch))
│        ConstructionBase.validate_setproperties_result(nt_new, nt, obj, patch)
│   %5 = ConstructionBase.typeof(obj)::Core.Const(Main.Foo.B)
│   %6 = ConstructionBase.constructorof(%5)::Core.Const(Main.Foo.B)
│   %7 = ConstructionBase.Tuple(nt_new)::Tuple%8 = Core._apply_iterate(Base.iterate, %6, %7)::Main.Foo.B
└──      return %8

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions