diff --git a/.ruby-version b/.ruby-version
index 860487c..ff365e0 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.7.1
+3.1.3
diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/78031180-13A0-4520-92AA-B9A7A0AA4431.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/78031180-13A0-4520-92AA-B9A7A0AA4431.plist
new file mode 100644
index 0000000..f415325
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/78031180-13A0-4520-92AA-B9A7A0AA4431.plist
@@ -0,0 +1,96 @@
+
+
+
+
+ classNames
+
+ EquatableByKeyPathPerformanceTests
+
+ testEqualityPerformance()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.016547
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+ HashableByKeyPathPerformanceTests
+
+ testEqualityPerformance()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.017176
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+ testHashPerformance()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.010787
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+ HashableByKeyPathTests
+
+ testEqualityPerformance()
+
+ com.apple.dt.XCTMetric_Clock.time.monotonic
+
+ baselineAverage
+ 0.026650
+ baselineIntegrationDisplayName
+ Local Baseline
+
+ com.apple.dt.XCTMetric_Memory.physical
+
+ baselineAverage
+ 3.276800
+ baselineIntegrationDisplayName
+ Local Baseline
+
+ com.apple.dt.XCTMetric_Memory.physical_peak
+
+ baselineAverage
+ 0.000000
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+ testHashPerformance()
+
+ com.apple.dt.XCTMetric_Clock.time.monotonic
+
+ baselineAverage
+ 0.029324
+ baselineIntegrationDisplayName
+ Local Baseline
+
+ com.apple.dt.XCTMetric_Memory.physical
+
+ baselineAverage
+ 3.276800
+ baselineIntegrationDisplayName
+ Local Baseline
+
+ com.apple.dt.XCTMetric_Memory.physical_peak
+
+ baselineAverage
+ 0.000000
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/Info.plist
new file mode 100644
index 0000000..f45b1d1
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/Info.plist
@@ -0,0 +1,33 @@
+
+
+
+
+ runDestinationsByUUID
+
+ 78031180-13A0-4520-92AA-B9A7A0AA4431
+
+ localComputer
+
+ busSpeedInMHz
+ 0
+ cpuCount
+ 1
+ cpuKind
+ Apple M1 Pro
+ cpuSpeedInMHz
+ 0
+ logicalCPUCoresPerPackage
+ 10
+ modelCode
+ MacBookPro18,1
+ physicalCPUCoresPerPackage
+ 10
+ platformIdentifier
+ com.apple.platform.macosx
+
+ targetArchitecture
+ arm64
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/HashableByKeyPath.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/HashableByKeyPath.xcscheme
index 7bad313..09595a6 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/HashableByKeyPath.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/HashableByKeyPath.xcscheme
@@ -53,6 +53,16 @@
ReferencedContainer = "container:">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.swift b/Package.swift
index 3a13f87..b66c83c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -9,5 +9,6 @@ let package = Package(
targets: [
.target(name: "HashableByKeyPath"),
.testTarget(name: "HashableByKeyPathTests", dependencies: ["HashableByKeyPath"]),
+ .testTarget(name: "HashableByKeyPathPerformanceTests", dependencies: ["HashableByKeyPath"]),
]
)
diff --git a/Sources/HashableByKeyPath/EquatabilityKeyPathAggregator.swift b/Sources/HashableByKeyPath/EquatabilityKeyPathAggregator.swift
deleted file mode 100644
index 02b6010..0000000
--- a/Sources/HashableByKeyPath/EquatabilityKeyPathAggregator.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-internal struct EquatabilityKeyPathAggregator: EquatableKeyPathConsumer {
-
- private typealias EquateClosure = (_ lhs: Root, _ rhs: Root) -> Bool
-
- private var closures: [EquateClosure] = []
-
- internal mutating func addEquatableKeyPath(_ keyPath: KeyPath) where KeyType: Equatable {
- closures.append({ lhs, rhs in
- return lhs[keyPath: keyPath] == rhs[keyPath: keyPath]
- })
- }
-
- internal mutating func addCustomEquator(forKeyPath keyPath: KeyPath, equator: @escaping (KeyType, KeyType) -> Bool) where KeyType: Equatable {
- closures.append { lhs, rhs in
- return equator(lhs[keyPath: keyPath], rhs[keyPath: keyPath])
- }
- }
-
- internal func evaluateEquality(lhs: Root, rhs: Root) -> Bool {
- return closures.allSatisfy { $0(lhs, rhs) }
- }
-
-}
diff --git a/Sources/HashableByKeyPath/EquatableByKeyPath.swift b/Sources/HashableByKeyPath/EquatableByKeyPath.swift
index f070f3f..c480bdb 100644
--- a/Sources/HashableByKeyPath/EquatableByKeyPath.swift
+++ b/Sources/HashableByKeyPath/EquatableByKeyPath.swift
@@ -2,22 +2,17 @@
A protocol that defines a single function that can be used to synthesise `Equatable` conformance.
*/
public protocol EquatableByKeyPath: Equatable {
-
/**
Add key paths to `consumer` that will be used for `Equatable` conformance.
- parameter consumer: The consumer to add the key paths to.
*/
static func addEquatableKeyPaths(to consumer: inout Consumer) where Consumer.Root == Self
-
}
extension EquatableByKeyPath {
-
public static func == (lhs: Self, rhs: Self) -> Bool {
- var aggregator = EquatabilityKeyPathAggregator()
- addEquatableKeyPaths(to: &aggregator)
- return aggregator.evaluateEquality(lhs: lhs, rhs: rhs)
+ var evaluator = EquatableByKeyPathEvaluator(lhs: lhs, rhs: rhs)
+ return evaluator.checkEquality()
}
-
}
diff --git a/Sources/HashableByKeyPath/EquatableByKeyPathEvaluator.swift b/Sources/HashableByKeyPath/EquatableByKeyPathEvaluator.swift
new file mode 100644
index 0000000..abe2c11
--- /dev/null
+++ b/Sources/HashableByKeyPath/EquatableByKeyPathEvaluator.swift
@@ -0,0 +1,28 @@
+internal struct EquatableByKeyPathEvaluator: EquatableKeyPathConsumer {
+ private let lhs: Root
+ private let rhs: Root
+ private var hasFoundNotEqualProperty = false
+
+ init(lhs: Root, rhs: Root) {
+ self.lhs = lhs
+ self.rhs = rhs
+ }
+
+ internal mutating func checkEquality() -> Bool {
+ hasFoundNotEqualProperty = false
+ Root.addEquatableKeyPaths(to: &self)
+ return !hasFoundNotEqualProperty
+ }
+
+ internal mutating func addEquatableKeyPath(_ keyPath: KeyPath) where KeyType: Equatable {
+ guard !hasFoundNotEqualProperty else { return }
+
+ hasFoundNotEqualProperty = lhs[keyPath: keyPath] != rhs[keyPath: keyPath]
+ }
+
+ internal mutating func addCustomEquator(forKeyPath keyPath: KeyPath, equator: @escaping (KeyType, KeyType) -> Bool) where KeyType: Equatable {
+ guard !hasFoundNotEqualProperty else { return }
+
+ hasFoundNotEqualProperty = !equator(lhs[keyPath: keyPath], rhs[keyPath: keyPath])
+ }
+}
diff --git a/Sources/HashableByKeyPath/EquatableKeyPathConsumer.swift b/Sources/HashableByKeyPath/EquatableKeyPathConsumer.swift
index c5257c1..79c6157 100644
--- a/Sources/HashableByKeyPath/EquatableKeyPathConsumer.swift
+++ b/Sources/HashableByKeyPath/EquatableKeyPathConsumer.swift
@@ -2,7 +2,6 @@
A protocol that defines a function that can be used to add key paths from the `Root` type to `Equatable` properties.
*/
public protocol EquatableKeyPathConsumer {
-
/// The root type of the object that will be equated.
associatedtype Root
@@ -19,5 +18,4 @@ public protocol EquatableKeyPathConsumer {
- parameter keyPath: The key to include when equating 2 instances of `Root`.
*/
mutating func addCustomEquator(forKeyPath keyPath: KeyPath, equator: @escaping (KeyType, KeyType) -> Bool) where KeyType: Equatable
-
}
diff --git a/Sources/HashableByKeyPath/HashableByKeyPath.swift b/Sources/HashableByKeyPath/HashableByKeyPath.swift
index 9b9a634..c59ba65 100644
--- a/Sources/HashableByKeyPath/HashableByKeyPath.swift
+++ b/Sources/HashableByKeyPath/HashableByKeyPath.swift
@@ -23,9 +23,9 @@ extension HashableByKeyPath {
}
public func hash(into hasher: inout Hasher) {
- var hashableKeyPathAggregator = HashableKeyPathAggregator()
- Self.addHashableKeyPaths(to: &hashableKeyPathAggregator)
- return hashableKeyPathAggregator.hashValues(from: self, into: &hasher)
+ var keyPathHasher = KeyPathHasher(root: self, hasher: hasher)
+ Self.addHashableKeyPaths(to: &keyPathHasher)
+ hasher = keyPathHasher.hasher
}
}
diff --git a/Sources/HashableByKeyPath/HashableKeyPathAggregator.swift b/Sources/HashableByKeyPath/HashableKeyPathAggregator.swift
deleted file mode 100644
index bafe4ca..0000000
--- a/Sources/HashableByKeyPath/HashableKeyPathAggregator.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-internal struct HashableKeyPathAggregator: HashableKeyPathConsumer {
-
- private var closures: [(_ root: Root, _ hasher: inout Hasher) -> Void] = []
-
- private typealias EquateClosure = (_ lhs: Root, _ rhs: Root) -> Bool
-
- private var equateClosures: [EquateClosure] = []
-
- internal init() {}
-
- internal mutating func addHashableKeyPath(_ keyPath: KeyPath) where KeyType: Hashable {
- closures.append({ root, hasher in
- return root[keyPath: keyPath].hash(into: &hasher)
- })
- equateClosures.append { lhs, rhs in
- return lhs[keyPath: keyPath] == rhs[keyPath: keyPath]
- }
- }
-
- internal mutating func addCustomEquator(forKeyPath keyPath: KeyPath, equator: @escaping (KeyType, KeyType) -> Bool) where KeyType : Hashable {
- closures.append({ root, hasher in
- return root[keyPath: keyPath].hash(into: &hasher)
- })
- equateClosures.append { lhs, rhs in
- return equator(lhs[keyPath: keyPath], rhs[keyPath: keyPath])
- }
- }
-
- internal func hashValues(from root: Root, into hasher: inout Hasher) {
- closures.forEach { $0(root, &hasher) }
- }
-
- internal func evaluateEquality(lhs: Root, rhs: Root) -> Bool {
- return equateClosures.allSatisfy { $0(lhs, rhs) }
- }
-}
diff --git a/Sources/HashableByKeyPath/HashableKeyPathForwarder.swift b/Sources/HashableByKeyPath/HashableKeyPathForwarder.swift
index f73142a..88e3773 100644
--- a/Sources/HashableByKeyPath/HashableKeyPathForwarder.swift
+++ b/Sources/HashableByKeyPath/HashableKeyPathForwarder.swift
@@ -1,5 +1,4 @@
internal struct HashableKeyPathForwarder: HashableKeyPathConsumer where Consumer.Root == Root {
-
internal typealias KeyPathListener = (_ keyPath: KeyPath) -> Void
internal var equatableKeyPathConsumer: Consumer
@@ -8,12 +7,13 @@ internal struct HashableKeyPathForwarder(_ keyPath: KeyPath) where KeyType: Hashable {
equatableKeyPathConsumer.addEquatableKeyPath(keyPath)
}
+ @inlinable
internal mutating func addCustomEquator(forKeyPath keyPath: KeyPath, equator: @escaping (KeyType, KeyType) -> Bool) where KeyType: Hashable {
equatableKeyPathConsumer.addCustomEquator(forKeyPath: keyPath, equator: equator)
}
-
}
diff --git a/Sources/HashableByKeyPath/KeyPathHasher.swift b/Sources/HashableByKeyPath/KeyPathHasher.swift
new file mode 100644
index 0000000..13a1cc2
--- /dev/null
+++ b/Sources/HashableByKeyPath/KeyPathHasher.swift
@@ -0,0 +1,19 @@
+internal struct KeyPathHasher: HashableKeyPathConsumer {
+ internal private(set) var hasher: Hasher
+
+ private let root: Root
+
+ internal init(root: Root, hasher: Hasher) {
+ self.root = root
+ self.hasher = hasher
+ }
+
+ @inlinable
+ internal mutating func addHashableKeyPath(_ keyPath: KeyPath) where KeyType: Hashable {
+ hasher.combine(root[keyPath: keyPath])
+ }
+
+ internal mutating func addCustomEquator(forKeyPath keyPath: KeyPath, equator: @escaping (KeyType, KeyType) -> Bool) where KeyType: Hashable {
+ // `KeyPathHasher` is never used for equality.
+ }
+}
diff --git a/Tests/HashableByKeyPathPerformanceTests/EquatableByKeyPathPerformanceTests.swift b/Tests/HashableByKeyPathPerformanceTests/EquatableByKeyPathPerformanceTests.swift
new file mode 100644
index 0000000..0ecd55e
--- /dev/null
+++ b/Tests/HashableByKeyPathPerformanceTests/EquatableByKeyPathPerformanceTests.swift
@@ -0,0 +1,52 @@
+import XCTest
+import HashableByKeyPath
+
+final class EquatableByKeyPathPerformanceTests: XCTestCase {
+ func testEqualityPerformance() {
+ final class Foo: EquatableByKeyPath {
+ static func addEquatableKeyPaths(to consumer: inout Consumer) where Consumer.Root == Foo, Consumer: EquatableKeyPathConsumer {
+ consumer.addEquatableKeyPath(\.propertyA)
+ consumer.addEquatableKeyPath(\.propertyB)
+ consumer.addEquatableKeyPath(\.propertyC)
+ consumer.addEquatableKeyPath(\.propertyD)
+ consumer.addEquatableKeyPath(\.propertyE)
+ consumer.addEquatableKeyPath(\.propertyF)
+ consumer.addEquatableKeyPath(\.propertyG)
+ consumer.addEquatableKeyPath(\.child)
+ }
+
+ let propertyA: String
+ let propertyB: String
+ let propertyC: String
+ let propertyD: String
+ let propertyE: String
+ let propertyF: String
+ var propertyG: String
+ let child: Foo?
+
+ init(level: Int = 0) {
+ self.propertyA = "propertyA\(level)"
+ self.propertyB = "propertyB\(level)"
+ self.propertyC = "propertyC\(level)"
+ self.propertyD = "propertyD\(level)"
+ self.propertyE = "propertyE\(level)"
+ self.propertyF = "propertyF\(level)"
+ self.propertyG = "propertyG\(level)"
+ if level >= 5 {
+ child = nil
+ } else {
+ child = Foo(level: level + 1)
+ }
+ }
+ }
+
+ let foo1 = Foo()
+ let foo2 = Foo()
+
+ measure {
+ for _ in 0..<1000 {
+ _blackHole(foo1 == foo2)
+ }
+ }
+ }
+}
diff --git a/Tests/HashableByKeyPathPerformanceTests/HashableByKeyPathPerformanceTests.swift b/Tests/HashableByKeyPathPerformanceTests/HashableByKeyPathPerformanceTests.swift
new file mode 100644
index 0000000..3dbedaa
--- /dev/null
+++ b/Tests/HashableByKeyPathPerformanceTests/HashableByKeyPathPerformanceTests.swift
@@ -0,0 +1,99 @@
+import XCTest
+import HashableByKeyPath
+
+final class HashableByKeyPathPerformanceTests: XCTestCase {
+ func testHashPerformance() {
+ final class Foo: HashableByKeyPath {
+ static func addHashableKeyPaths(to consumer: inout Consumer) where Consumer.Root == Foo, Consumer: HashableKeyPathConsumer {
+ consumer.addHashableKeyPath(\.propertyA)
+ consumer.addHashableKeyPath(\.propertyB)
+ consumer.addHashableKeyPath(\.propertyC)
+ consumer.addHashableKeyPath(\.propertyD)
+ consumer.addHashableKeyPath(\.propertyE)
+ consumer.addHashableKeyPath(\.propertyF)
+ consumer.addHashableKeyPath(\.propertyG)
+ consumer.addHashableKeyPath(\.child)
+ }
+
+ let propertyA: String
+ let propertyB: String
+ let propertyC: String
+ let propertyD: String
+ let propertyE: String
+ let propertyF: String
+ let propertyG: String
+ private let child: Foo?
+
+ init(level: Int = 0) {
+ self.propertyA = "propertyA\(level)"
+ self.propertyB = "propertyB\(level)"
+ self.propertyC = "propertyC\(level)"
+ self.propertyD = "propertyD\(level)"
+ self.propertyE = "propertyE\(level)"
+ self.propertyF = "propertyF\(level)"
+ self.propertyG = "propertyG\(level)"
+ if level >= 5 {
+ child = nil
+ } else {
+ child = Foo(level: level + 1)
+ }
+ }
+ }
+
+ let foo1 = Foo()
+
+ measure {
+ for _ in 0..<1000 {
+ _blackHole(foo1.hashValue)
+ }
+ }
+ }
+
+ func testEqualityPerformance() {
+ final class Foo: HashableByKeyPath {
+ static func addHashableKeyPaths(to consumer: inout Consumer) where Consumer.Root == Foo, Consumer: HashableKeyPathConsumer {
+ consumer.addHashableKeyPath(\.propertyA)
+ consumer.addHashableKeyPath(\.propertyB)
+ consumer.addHashableKeyPath(\.propertyC)
+ consumer.addHashableKeyPath(\.propertyD)
+ consumer.addHashableKeyPath(\.propertyE)
+ consumer.addHashableKeyPath(\.propertyF)
+ consumer.addHashableKeyPath(\.propertyG)
+ consumer.addHashableKeyPath(\.child)
+ }
+
+ let propertyA: String
+ let propertyB: String
+ let propertyC: String
+ let propertyD: String
+ let propertyE: String
+ let propertyF: String
+ var propertyG: String
+ let child: Foo?
+
+ init(level: Int = 0) {
+ self.propertyA = "propertyA\(level)"
+ self.propertyB = "propertyB\(level)"
+ self.propertyC = "propertyC\(level)"
+ self.propertyD = "propertyD\(level)"
+ self.propertyE = "propertyE\(level)"
+ self.propertyF = "propertyF\(level)"
+ self.propertyG = "propertyG\(level)"
+ if level >= 5 {
+ child = nil
+ } else {
+ child = Foo(level: level + 1)
+ }
+ }
+ }
+
+ let foo1 = Foo()
+ let foo2 = Foo()
+
+ measure {
+ for _ in 0..<1000 {
+ _blackHole(foo1 == foo2)
+ }
+ }
+ }
+}
diff --git a/Tests/HashableByKeyPathPerformanceTests/_blackHole.swift b/Tests/HashableByKeyPathPerformanceTests/_blackHole.swift
new file mode 100644
index 0000000..9f95050
--- /dev/null
+++ b/Tests/HashableByKeyPathPerformanceTests/_blackHole.swift
@@ -0,0 +1,9 @@
+/// An empty function used to prevent the compiler optimising away the value
+/// passed in to it.
+@inline(never)
+func _blackHole(_ x: Int) {}
+
+/// An empty function used to prevent the compiler optimising away the value
+/// passed in to it.
+@inline(never)
+func _blackHole(_ x: Bool) {}