Faster Equatable and Hashable conformances


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 Case | Time (ms) |
1. Equatable w/ Identifiable | 1.22 |
2. Equatable (Synthesized) | 3.83 |
3. Equatable (Synthesized, Identical) | 11.61 |
4. Hashable w/ Identifiable | 6.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:
Test Object:
Company
with 1,000+ employeesNesting: 3 levels deep (
Company
→Employees
→Profile
)Iterations: 1,000,000 (for all tests)
Environment: Release build, MacBook Air M4, macOS Tahoe 26.0
Benchmark Code: https://github.com/erkekin/benchmark-equatable-and-hashable-with-id
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
Subscribe to my newsletter
Read articles from Erk Ekin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
