I have this Equatable implementation:

    static func == (lhs: DayViewModel, rhs: DayViewModel) -> Bool {
        lhs.id == rhs.id
        && lhs.referenceDate == rhs.referenceDate
        && lhs.dayOfMonth == rhs.dayOfMonth
        && lhs.dayOfWeekMedium == rhs.dayOfWeekMedium
        && lhs.dayOfWeek == rhs.dayOfWeek
        && lhs.isToday == rhs.isToday
        && lhs.isWeekend == rhs.isWeekend
        && lhs.isFirstDayOfWeek == rhs.isFirstDayOfWeek
        && lhs.weekNumber == rhs.weekNumber
        && lhs.year == rhs.year
        && lhs.dayOfYear == rhs.dayOfYear
        && lhs.events == rhs.events
        && lhs.birthdays == rhs.birthdays
        && lhs.anniversaries == rhs.anniversaries
        && lhs.lunarEvent == rhs.lunarEvent
        && lhs.marginMark == rhs.marginMark
        && lhs.isHoliday == rhs.isHoliday
        && lhs.astroDayViewModel == rhs.astroDayViewModel
    }

It takes 200+ ms to type check.

Some if the properties are Optional, some are arrays. What is the best approach to get the compile time down while not completely destroying the runtime performance?

Approach A

I have tried splitting into several separate statements but even with only 4 comparisons in each, the compile time is still above 100 ms.

     static func == (lhs: DayViewModel, rhs: DayViewModel) -> Bool {
     let a = lhs.id == rhs.id
         && lhs.referenceDate == rhs.referenceDate
         && lhs.dayOfMonth == rhs.dayOfMonth
         && lhs.dayOfWeekMedium == rhs.dayOfWeekMedium
     
     let b = lhs.dayOfWeek == rhs.dayOfWeek
         && lhs.isToday == rhs.isToday
         && lhs.isWeekend == rhs.isWeekend
         && lhs.isFirstDayOfWeek == rhs.isFirstDayOfWeek

     ...

     return a && b && c && d && e
     }

Aproach B

    static func == (lhs: DayViewModel, rhs: DayViewModel) -> Bool {
        guard lhs.id == rhs.id else { return false }
        guard lhs.referenceDate == rhs.referenceDate else { return false }
        ...
        return true
    }

which seems ok readable.

Approach C

    static func == (lhs: DayViewModel, rhs: DayViewModel) -> Bool {
        let b: [Bool] = [
            lhs.id.rawValue == rhs.id.rawValue,
            lhs.referenceDate == rhs.referenceDate,
            ...
        ]

        return b.allSatisfy(\.self)
    }

How much is the overall compilation slowdown with Option A and Option B?
Definitely don't use Option C, it's an extra memory allocation and besides it doesn't short-circuit evaluation and calculates all == first.

I'm assuming the underlying model here is a class, or else there's some other reason you can't rely on the synthesized Equatable implementation. So another option would be to factor out the equality-relevant state into a separate struct and then rely on Equatable synthesis.

2 Likes

This seems like something we shouldn't have any trouble type-checking quickly. Can you make this a standalone test case and share it with us?

1 Like

+1

Yet another approach would be making a tuple:

(lhs.a, lhs.b, ...) == (rhs.a, rhs.b, ...)

but it's probably not as fast as Option B.

It also won't work if you have more than six properties to compare.

1 Like

FWIW, the way it is stated now "the following example compares tuples made up of 6 components" doesn't look a limit, just an example.

Good to hear. I will make the most minimal example that still reproduces it and share it.

1 Like

I have tried to isolate the slow func == and I do not understand the results.

I made a new Xcode project and copied the DayViewModel class to it. I also copied the structs referenced as properties - 1 final class, 4 structs, 4 enums total involved.

So I would expect a result very similar to my full project. In the full project Xcode reports ~ 240-250 ms for the func == . In the clean project 120-130 ms, both clean builds. I am surprised that the clean project has the same function so much faster type checked.

The class and one of the structs had SwiftUI Color properties. I changed the type of these to String and removed the SwiftUI imports. The reproduction project is now down to ~60 ms for the func ==. Again I am surprised that this change makes such a difference.

If I begin to remove any property the time drops for each one and I cannot seem to locate any single that makes a big difference.

The results are the same for Xcode 15.4 and 16.0 beta 3 (16A5202i)

Do you think this is isolated enough to be of use for you if I submit it?

This variant will not compile.

Binary operator '==' cannot be applied to two '(DayViewModel.SID, Date, String, String, String, Bool, Bool, Bool, Int, Int, DateComponents, [EventViewModel], [AnniversaryViewModel], [AnniversaryViewModel], LunarEvent?, Color?, Bool, AstroDayViewModel)' (aka '(Identifier<DayViewModel>, Date, String, String, String, Bool, Bool, Bool, Int, Int, DateComponents, Array<EventViewModel>, Array<AnniversaryViewModel>, Array<AnniversaryViewModel>, Optional<LunarEvent>, Optional<Color>, Bool, AstroDayViewModel)') operands

Type inference is largely lazy in the compiler, so when you see large time values like this, it's often due to the compiler doing a bunch of necessary but one off work. If you create a second type with all the same properties, does it also show a time warning? I'd expect not, or at a much lower time.

2 Likes

It looks like the original "6D" limitation was motivated by the impact the codegen would have on codesize.

It is possible that parameter packs might enable Swift to ship these all with "just one" function (generic across packs).

1 Like

Just brainstorming here… by any chance did DayViewModel (in the original module) have dependencies on public types that were located in other modules?

The codegen is currently suboptimal, so we can’t get rid of concrete overloads yet, but it does work.

1 Like

If I make a new class with exact same properties and func == implementation, it take the same time to type check as the original and the original still takes the same time.

No. Not besides SwiftUI.