From 55a7e822df002ec403c133f17565b3e84885606a Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Wed, 19 Oct 2022 18:53:48 +0100 Subject: [PATCH 1/9] Improve performance of hashing Roughly 2x faster by removing the need to store closures --- .../HashableByKeyPath/HashableByKeyPath.swift | 6 +-- .../HashableKeyPathAggregator.swift | 36 ------------- Sources/HashableByKeyPath/KeyPathHasher.swift | 19 +++++++ .../HashableByKeyPathTests.swift | 51 +++++++++++++++++++ 4 files changed, 73 insertions(+), 39 deletions(-) delete mode 100644 Sources/HashableByKeyPath/HashableKeyPathAggregator.swift create mode 100644 Sources/HashableByKeyPath/KeyPathHasher.swift create mode 100644 Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift 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/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/HashableByKeyPathTests/HashableByKeyPathTests.swift b/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift new file mode 100644 index 0000000..46caeeb --- /dev/null +++ b/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift @@ -0,0 +1,51 @@ +import XCTest +import HashableByKeyPath + +final class HashableByKeyPathTests: 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..<100 { + _ = foo1.hashValue + } + } + } +} From bfca6f697049223b0f506aaa371812b3747b8f8b Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Wed, 19 Oct 2022 19:32:03 +0100 Subject: [PATCH 2/9] Add equality performance test --- .../HashableByKeyPathTests.swift | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift b/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift index 46caeeb..2fed8cd 100644 --- a/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift +++ b/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift @@ -43,9 +43,57 @@ final class HashableByKeyPathTests: XCTestCase { let foo1 = Foo() measure { - for _ in 0..<100 { + for _ in 0..<1000 { _ = 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 { + _ = foo1 == foo2 + } + } + } } From 21f9a9adf87cb6809b7e08603f3d11f6e8a6de74 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Wed, 19 Oct 2022 19:32:18 +0100 Subject: [PATCH 3/9] Allow functions to be inlined --- Sources/HashableByKeyPath/HashableKeyPathForwarder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) } - } From a7e982db0ef9fe14bd9a2362e63097ee6d6eb0f1 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Fri, 9 Dec 2022 22:55:28 +0000 Subject: [PATCH 4/9] Remove use of closures when checking equality --- .../EquatabilityKeyPathAggregator.swift | 23 --------------- .../EquatableByKeyPath.swift | 5 ++-- .../EquatableByKeyPathEvaluator.swift | 28 +++++++++++++++++++ 3 files changed, 30 insertions(+), 26 deletions(-) delete mode 100644 Sources/HashableByKeyPath/EquatabilityKeyPathAggregator.swift create mode 100644 Sources/HashableByKeyPath/EquatableByKeyPathEvaluator.swift 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..7cf24d5 100644 --- a/Sources/HashableByKeyPath/EquatableByKeyPath.swift +++ b/Sources/HashableByKeyPath/EquatableByKeyPath.swift @@ -15,9 +15,8 @@ public protocol EquatableByKeyPath: Equatable { 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]) + } +} From 744662b0492b3d0b80b8cd4ee4b3c188675ff9e5 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Fri, 9 Dec 2022 22:55:40 +0000 Subject: [PATCH 5/9] Remove extra empty lines --- Sources/HashableByKeyPath/EquatableByKeyPath.swift | 4 ---- Sources/HashableByKeyPath/EquatableKeyPathConsumer.swift | 2 -- 2 files changed, 6 deletions(-) diff --git a/Sources/HashableByKeyPath/EquatableByKeyPath.swift b/Sources/HashableByKeyPath/EquatableByKeyPath.swift index 7cf24d5..c480bdb 100644 --- a/Sources/HashableByKeyPath/EquatableByKeyPath.swift +++ b/Sources/HashableByKeyPath/EquatableByKeyPath.swift @@ -2,21 +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 evaluator = EquatableByKeyPathEvaluator(lhs: lhs, rhs: rhs) return evaluator.checkEquality() } - } 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 - } From 2ea5dfd9e2f685eef4c7611072fe6e35315ea2a3 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Sat, 10 Dec 2022 13:07:08 +0000 Subject: [PATCH 6/9] Add performance tests --- ...78031180-13A0-4520-92AA-B9A7A0AA4431.plist | 96 ++++++++++++++++++ .../Info.plist | 33 +++++++ .../xcschemes/HashableByKeyPath.xcscheme | 10 ++ ...HashableByKeyPathPerformanceTests.xcscheme | 52 ++++++++++ Package.swift | 1 + .../EquatableByKeyPathPerformanceTests.swift | 52 ++++++++++ .../HashableByKeyPathPerformanceTests.swift | 99 +++++++++++++++++++ .../_blackHole.swift | 9 ++ 8 files changed, 352 insertions(+) create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/78031180-13A0-4520-92AA-B9A7A0AA4431.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/HashableByKeyPathPerformanceTests.xcbaseline/Info.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/HashableByKeyPathPerformanceTests.xcscheme create mode 100644 Tests/HashableByKeyPathPerformanceTests/EquatableByKeyPathPerformanceTests.swift create mode 100644 Tests/HashableByKeyPathPerformanceTests/HashableByKeyPathPerformanceTests.swift create mode 100644 Tests/HashableByKeyPathPerformanceTests/_blackHole.swift 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..4c23e30 --- /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.027264 + baselineIntegrationDisplayName + Local Baseline + + + + HashableByKeyPathPerformanceTests + + testEqualityPerformance() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.028335 + baselineIntegrationDisplayName + Local Baseline + + + testHashPerformance() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.030798 + 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/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) {} From 7e246d72d074185cc94377530b3ffb4acfe4b5fd Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Sat, 10 Dec 2022 13:07:28 +0000 Subject: [PATCH 7/9] Use ruby version 3.1.3 --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d5da2b0727215659177777c258492942262e4e60 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Sat, 10 Dec 2022 13:09:38 +0000 Subject: [PATCH 8/9] Remove duplicate performance test --- .../HashableByKeyPathTests.swift | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift diff --git a/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift b/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift deleted file mode 100644 index 2fed8cd..0000000 --- a/Tests/HashableByKeyPathTests/HashableByKeyPathTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -import XCTest -import HashableByKeyPath - -final class HashableByKeyPathTests: 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 { - _ = 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 { - _ = foo1 == foo2 - } - } - } -} From 182a6e7b380759dbefc47e56ecada20b246b1a44 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Sat, 10 Dec 2022 13:09:50 +0000 Subject: [PATCH 9/9] Update baselines --- .../78031180-13A0-4520-92AA-B9A7A0AA4431.plist | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 4c23e30..f415325 100644 --- 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 @@ -11,7 +11,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.027264 + 0.016547 baselineIntegrationDisplayName Local Baseline @@ -24,7 +24,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.028335 + 0.017176 baselineIntegrationDisplayName Local Baseline @@ -34,7 +34,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.030798 + 0.010787 baselineIntegrationDisplayName Local Baseline