Function with a single method acting on Union{T1, T2} or another function with two methods, for T1 and T2?

I have the following code:

struct Square{T<:Real}
    width::T = 1
end

struct Rectangle{T<:Real}
    width::T = 1
    height::T = 2
end

Quadrilateral = Union{Square, Rectangle}

function area1(q::Quadrilateral)
    if q isa Square
        return q.width^2
    elseif q isa Rectangle
        return q.width * q.height
    end
end

function area2(q::Square)
    q.width^2
end

function area2(q::Rectangle)
    q.width * q.height
end

When I use @code_warntype, upon the same concrete Quadrilateral, the output for the area1 function, which uses the Union{Square, Rectangle} type, is longer and seems to be a little more contrived than the corresponding output for the area2 function. Using a naive benchmarking does not, however, seem to give appreciable difference.
Which strategy should I follow/prefer, taking into account better code legibility and performance? Right now, I am inclined to using area2, with its specific methods, according to the different calling signatures; does that sound reasonable?

This is similar to

Generically, the area2 approach has two benefits:

  • it can be expanded to additional shapes
  • it guarantees compile time dispatch (if the rest of the code is type stable)
    Perhaps the logical approach would be to set up Square and Rectangle as subtypes of Quadrilateral. But that depends on what else you are doing with those types.
3 Likes

Both versions should perform identically, you are just viewing the typed IR before optimization. They actually produce the same optimized code for me:

julia> @code_warntype area1(Square(1))
Variables
  #self#::Core.Compiler.Const(area1, false)
  q::Square{Int64}

Body::Int64
1 ─ %1 = (q isa Main.Square)::Core.Compiler.Const(true, false)
β”‚        %1
β”‚   %3 = Base.getproperty(q, :width)::Int64
β”‚   %4 = Core.apply_type(Base.Val, 2)::Core.Compiler.Const(Val{2}, false)
β”‚   %5 = (%4)()::Core.Compiler.Const(Val{2}(), false)
β”‚   %6 = Base.literal_pow(Main.:^, %3, %5)::Int64
└──      return %6
2 ─      Core.Compiler.Const(:(q isa Main.Rectangle), false)
β”‚        Core.Compiler.Const(:(%8), false)
β”‚        Core.Compiler.Const(:(Base.getproperty(q, :width)), false)
β”‚        Core.Compiler.Const(:(Base.getproperty(q, :height)), false)
β”‚        Core.Compiler.Const(:(%10 * %11), false)
β”‚        Core.Compiler.Const(:(return %12), false)
└──      Core.Compiler.Const(:(return), false)

julia> @code_warntype optimize=true area1(Square(1))
Variables
  #self#::Core.Compiler.Const(area1, false)
  q::Square{Int64}

Body::Int64
1 ─ %1 = Base.getfield(q, :width)::Int64
β”‚   %2 = Base.mul_int(%1, %1)::Int64
└──      return %2

julia> @code_warntype optimize=true area2(Square(1))
Variables
  #self#::Core.Compiler.Const(area2, false)
  q::Square{Int64}

Body::Int64
1 ─ %1 = Base.getfield(q, :width)::Int64
β”‚   %2 = Base.mul_int(%1, %1)::Int64
└──      return %2
2 Likes

One of approaches here, since both structs are immutable, is to define accessor functions width and height and make a single generic function area using accessors:

width(q::Union{Rectangle, Square}) = q.width
height(q::Square) = q.width
height(q::Rectangle) = q.height

area(q::Union{Rectangle, Square}) = width(q) * height(q)
4 Likes

For clarification: are you asking about the generic pattern or about the actual code that you posted? I think this will affect the answer.

@hendri54: I do not understand exactly what you mean… At any rate, any further comments are much welcome. Your former reply helped a lot and I read the link you mentioned there.

Likewise, I enjoyed the flag optimize=true to @code_warntype which @simeonschaub used in his post.

I asked because some replies (especially mine) discussed the general pattern (similar to the thread I pointed to earlier), while others discussed how to specifically calculate areas.
This matters, for example, for the question whether one can rely on compiler optimization to remove the unused branch in each call to area1.

My actual post was only a minimal working example (MWE). In fact, I am using that generic pattern in a larger code; I am not particularly concerned with this naive area example, but I think it might make things stripped down to the essential matter… I would like to hear any more generic comments or pointers anyone might deem relevant