Faster Equatable and Hashable conformances

Erk EkinErk Ekin
5 min read

Compiler-synthesized conformance in Swift is a powerful tool that accelerates development by reducing boilerplate code. For protocols like Equatable and Hashable, the compiler can automatically generate the required implementations—a convenience that is widely utilized. However, this automation, like any abstraction, comes with trade-offs. The default behavior can introduce significant and non-obvious performance bottlenecks, particularly in systems that rely on a clear concept of object identity.

This analysis examines the default synthesis mechanism, identifies its performance characteristics, and proposes an architectural pattern for specific use cases where performance is critical.

The Performance Cost of Synthesized Conformance

When the Swift compiler synthesizes conformance for Equatable and Hashable, it applies a simple, universal rule: every stored property of the type is included in the comparison or hashing logic. For a struct with a few value-type properties, the overhead is often negligible. However, for more complex data structures, the cost can become substantial.

Consider a model that includes a collection:

struct Company {
    let id: UUID
    let name: String
    let address: Address // Address might be a complex struct
    let employees: [User] // A potentially large collection
}

If Company conforms to Equatable, a comparison between two instances (companyA == companyB) will trigger a member-wise comparison of all properties. This includes iterating through the entire employees array and comparing each User object. Such an operation's complexity is at least O(N), where N is the number of elements in the collection, and can be worse depending on the complexity of the elements themselves.

In performance-sensitive contexts, such as the diffing algorithms used to update SwiftUI views, these O(N) comparisons can become a significant performance hotspot, leading to dropped frames and unresponsive user interfaces. The same complexity issue applies to Hashable conformance, affecting the performance of Set and Dictionary operations.

The Identifiable Protocol and Value Equality

Modern Swift development, particularly with frameworks like SwiftUI and Combine, heavily utilizes the Identifiable protocol.

public protocol Identifiable {
    associatedtype ID: Hashable
    var id: ID { get }
}

The Identifiable protocol asserts that a type has a single, canonical property representing its stable identity. If two instances share the same id, they represent the same conceptual entity, even if their other properties differ (e.g., a mutable state that has been updated).

The compiler's synthesized Equatable implementation, however, checks every property for value equality. This is distinct from identity. The default behavior is not wrong, but in cases where a full member-wise comparison isn't needed, it can be computationally expensive.

Verifying the Compiler's Behavior

An analysis of the Swift open-source repository confirms that the derivation logic for Equatable and Hashable is generalized and does not account for the semantic meaning of Identifiable.

The relevant logic resides in the compiler source file DerivedConformanceEquatableHashable.cpp. When synthesizing conformance, the compiler's primary concern is to verify that all of a type's stored properties themselves conform to Hashable before generating the combined hash.

// Refuse to synthesize Hashable if type isn't a struct or enum, or if it
// has non-Hashable stored properties/associated values.
auto hashableProto = Context.getProtocol(KnownProtocolKind::Hashable);
if (!canDeriveConformance(getConformanceContext(), Nominal,
                          hashableProto)) {
  // ...error handling...
}
// see: https://github.com/swiftlang/swift/blob/main/lib/Sema/DerivedConformanceEquatableHashable.cpp

The same is true for Equatable. As the source demonstrates, the validation step is mechanical: it checks for protocol conformance on all members. It does not contain any special logic to detect if the type also conforms to Identifiable and, if so, use the id property as the sole source for hashing and equality.

The compiler will always default to its member-wise synthesis unless a more specific implementation—like the one provided by our extensions—is available to satisfy the protocol requirements first.

A note on SwiftUI’s Equatable usage

SwiftUI's rendering efficiency is deeply tied to the Equatable protocol. When your model or state type conforms to Equatable, SwiftUI can perform a quick check, oldItem == newItem, to determine if a value has actually changed. If items are equal, it smartly skips re-rendering the associated view, leading to significantly better performance by only updating what's absolutely necessary. Conversely, if your model does not conform to Equatable, SwiftUI lacks this comparison mechanism. In such types do not use this technique with Identifiable as changes on varying parameters won’t be reflected to view.

To ensure data consistency, SwiftUI must assume that the item might have changed and will consequently re-render the view for that item, or even the entire view hierarchy, every time, leading to unnecessary redraws and reduced performance. Therefore, understanding and leveraging Equatable correctly is crucial for building performant SwiftUI applications.

Performance Benchmarks: The Numbers

To quantify the performance characteristics of both approaches, I created a custom benchmarking suite implemented directly in main.swift. The tests were conducted on a Company model containing over 1,000 employees, each with nested profile information.

Test CaseTime (ms)
1. Equatable w/ Identifiable1.22
2. Equatable (Synthesized)3.83
3. Equatable (Synthesized, Identical)11.61
4. Hashable w/ Identifiable6.44
5. Hashable (Synthesized)195,413.01

Performance Comparison

  • Synthesized Equatable is 3x slower than the Identifiable-based implementation.

  • Synthesized Hashable is 30K times slower than the Identifiable-based implementation.

Test Configuration:

The results demonstrate that the Identifiable-based implementation maintains consistent O(1) performance by focusing solely on the id property, while the synthesized implementation processes every property in the object graph. This difference becomes particularly relevant in performance-sensitive contexts like SwiftUI's diffing algorithms or when working with large collections in Set or Dictionary types.

References

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md

https://stackoverflow.com/questions/64279211/is-it-right-to-conform-hashable-by-only-taking-id-into-consideration

0
Subscribe to my newsletter

Read articles from Erk Ekin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erk Ekin
Erk Ekin