Ways to reduce compilation time incurred by highly overloaded functions (==)?

Hi there, I have a question about reducing compilation times of highly overloaded functions (==). Specifically, about the potential impact of reducing the number of overloads in a build using the Swift 6.3 compiler. These questions build off of this post( Type checker impact of reducing occurences of highly overloaded functions (==)? ), which could be good context.

I’m looking for guidance on three approaches we have evaluated for reducing the compile-time cost associated with a very large number of concrete == declarations.

We are using Apple Swift 6.3.2, currently in Swift 5 language mode, and expect to adopt Swift 6.4 later. Our codebase contains roughly 17,000 generated model-specific == declarations. In one representative context, an unqualified reference such as _ = (==) reported approximately 500 candidates.

I understand that this does not necessarily mean every typed == expression fully type-checks every declaration. However, our measurements suggest that the size of the visible overload set can materially affect compilation.

From our understanding there appear to be two related but distinct costs:

  • Type-checking generated equality bodies containing many property-level == expressions.
  • The effect of exporting thousands of concrete == declarations on downstream modules that import those models.

Some generated equality functions exceed 200 ms of type-checking time. Our first approach of rewriting a chained expression as individual early returns helps substantially:

public static func == (lhs: Model, rhs: Model) -> Bool {
  if lhs.first != rhs.first { return false }
  if lhs.second != rhs.second { return false }
  if lhs.third != rhs.third { return false }
  return true
}

This is stable and relatively conservative, but it retains every model-specific == declaration. It therefore improves the generated function body without reducing the overload set seen by importing code.

We are considering three broader alternatives.

Approach 1: A shared Equatable-refining protocol

public protocol GeneratedEquatable: Equatable {
  static func isEqual(_ lhs: Self, _ rhs: Self) -> Bool
  }
  public extension GeneratedEquatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
       isEqual(lhs, rhs)
  }
}

Generated types implement the differently named requirement:

public final class Model: GeneratedEquatable {
  public static func isEqual(_ lhs: Model, _ rhs: Model) -> Bool {
    lhs.first == rhs.first &&
    lhs.second == rhs.second
  }
}

This preserves ordinary a == b syntax for callers, while replacing thousands of model-specific == declarations with one protocol-extension implementation.

In our experiment, this reduced both generated-model compilation and downstream consumer compilation. However, adding the additional protocol conformance to thousands of types increased app's binary size by ~1mb.

A couple clarifying questions about this approach:

Is one protocol-extension == genuinely represented as one source-level overload for lookup, regardless of how many types conform?
Is the binary growth expected from conformance descriptors and witness-table metadata?
Is there a supported way to obtain the shared-witness behavior without adding another protocol conformance to every type?

Approach 2: Retain ==, but use a uniquely named generic helper inside its body.
Our prototype uses a custom operator, although a named function should provide similar lookup behavior:

infix operator =*=: ComparisonPrecedence

public func =*= <T: Equatable>(lhs: T, rhs: T) -> Bool {
  lhs == rhs
}

Each generated type keeps its concrete ==, but property comparisons use the helper:

public static func == (lhs: Model, rhs: Model) -> Bool {
  lhs.first =*= rhs.first &&
  lhs.second =*= rhs.second
}

The generated body resolves one generic =*= candidate instead of resolving == against the large overload set. The == inside the helper is type-checked once in a generic T: Equatable context.

This was the fastest approach for compiling the generated Models module in our synthetic benchmark and produced no measurable binary-size increase. A named helper could avoid introducing unfamiliar operator syntax.

However, it retains all model-specific == declarations. Our downstream consumer benchmark therefore showed almost no improvement.
Is our explanation of why the generic helper is cheaper accurate?
Does this reliably dispatch through the Equatable witness after the helper has been compiled?
Are there optimization, specialization, or code-size concerns with using this pattern at this scale?

We also considered using @_implements to provide the witness under another name.
As an experiment, we tested:

public final class Model: Equatable {
  @implements(Equatable, ==(::))
  public static func isEqual( lhs: Model, _ rhs: Model) -> Bool {
    lhs.first == rhs.first &&
    lhs.second == rhs.second
  }
}

This directly conforms to Equatable while avoiding the additional protocol conformance overhead, and does not expose a model-specific declaration named ==. But we didn't seriously consider adopting this implementation due these attributed not being a supported language feature. Still curious to here thoughts about this approach and if some of my assumptions are wrong. Is there a stable language feature with equivalent named-witness behavior and are there plans for such a feature in new swift versions?

For testing the approaches we created synthetic benchmarks to test the different approaches, so we still can't say for sure how rolling these changes out would affect our codebase as a whole.

The results nevertheless appear to demonstrate the distinction we are concerned about:

  • Avoiding == inside generated bodies improves compilation of those bodies.
  • Removing concrete == declarations also improves downstream code containing unrelated ordinary equality expressions.

Questions for the compiler community

  1. Are we interpreting Swift’s operator lookup and constraint solving behavior correctly?
  2. Which of these patterns would compiler engineers consider the safest long term design for generated code?
  3. Is the shared protocol binary-size cost fundamental, or is there a way to avoid it?
  4. Are there relevant operator-lookup or constraint-solver changes in Swift 6.4 that could alter these trade offs?
  5. Are there compiler diagnostics or profiling tools we should use to validate how many candidates are actually considered at each expression?

Did you evaluate the various different forms of the generated == that were suggested in the thread you linked? It seemed like simply restructuring the generated code could virtually eliminate the == checking cost. Now, eliminating the cost of so many == overloads is another question, but I think there were suggestions there as well.