From 89deb2acb974a53a217282267dd7ae8c5453525e Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Wed, 28 May 2025 09:54:45 -0700 Subject: [PATCH 1/2] Add a fast-path for equality checking when the backing is empty or pointer-equal --- .../Essentials/BenchmarkEssentials.swift | 87 +++++++++++++++++++ Sources/FoundationEssentials/Data/Data.swift | 25 +++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index fcc25aab8..fc5ef7e55 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -34,4 +34,91 @@ let benchmarks = { assert(u1 != u2) } } + + // MARK: Data + + func createSomeData(_ length: Int) -> Data { + var d = Data(repeating: 42, count: length) + // Set a byte to be another value just so we know we have a unique pointer to the backing + // For maximum inefficiency in the not equal case, set the last byte + d[length - 1] = UInt8.random(in: UInt8.min.. TwoDatasBox in + let d1 = Data() + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataEqualInline", closure: { benchmark, box in + blackHole(box.d1 == box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(12) // Less than size of InlineData.Buffer + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataNotEqualInline", closure: { benchmark, box in + blackHole(box.d1 != box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(12) // Less than size of InlineData.Buffer + let d2 = createSomeData(12) + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataEqualLarge", closure: { benchmark, box in + blackHole(box.d1 == box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 8) + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataNotEqualLarge", closure: { benchmark, box in + blackHole(box.d1 != box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 8) + let d2 = createSomeData(1024 * 8) + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataEqualReallyLarge", closure: { benchmark, box in + blackHole(box.d1 == box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 1024 * 8) + let d2 = d1 + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + + Benchmark("DataNotEqualReallyLarge", closure: { benchmark, box in + blackHole(box.d1 != box.d2) + }, setup: { () -> TwoDatasBox in + let d1 = createSomeData(1024 * 1024 * 8) + let d2 = createSomeData(1024 * 1024 * 8) + let box = TwoDatasBox(d1: d1, d2: d2) + return box + }) + } diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index 178c7cfa3..e617d7528 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -2710,13 +2710,34 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect @inlinable // This is @inlinable as emission into clients is safe -- the concept of equality on Data will not change. public static func ==(d1 : Data, d2 : Data) -> Bool { let length1 = d1.count - if length1 != d2.count { + let length2 = d2.count + + // Unequal length data can never be equal + guard length1 == length2 else { return false } + + // See if both are empty + switch (d1._representation, d2._representation) { + case (.empty, .empty): + return true + default: + break + } + if length1 > 0 { return d1.withUnsafeBytes { (b1: UnsafeRawBufferPointer) in return d2.withUnsafeBytes { (b2: UnsafeRawBufferPointer) in - return memcmp(b1.baseAddress!, b2.baseAddress!, b2.count) == 0 + // If they have the same base address and same count, it is equal + let b1Address = b1.baseAddress! + let b2Address = b2.baseAddress! + + if b1Address == b2Address { + return true + } else { + // Compare the contents + return memcmp(b1.baseAddress!, b2.baseAddress!, b2.count) == 0 + } } } } From 7ea7b315dc448f9ac5b46985f38cacd094212f19 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Wed, 28 May 2025 10:22:23 -0700 Subject: [PATCH 2/2] Address review feedback --- Sources/FoundationEssentials/Data/Data.swift | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/FoundationEssentials/Data/Data.swift b/Sources/FoundationEssentials/Data/Data.swift index e617d7528..9f5bb7245 100644 --- a/Sources/FoundationEssentials/Data/Data.swift +++ b/Sources/FoundationEssentials/Data/Data.swift @@ -2709,22 +2709,23 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect /// Returns `true` if the two `Data` arguments are equal. @inlinable // This is @inlinable as emission into clients is safe -- the concept of equality on Data will not change. public static func ==(d1 : Data, d2 : Data) -> Bool { - let length1 = d1.count - let length2 = d2.count - - // Unequal length data can never be equal - guard length1 == length2 else { - return false - } - // See if both are empty switch (d1._representation, d2._representation) { case (.empty, .empty): return true default: + // Continue on to checks below break } + let length1 = d1.count + let length2 = d2.count + + // Unequal length data can never be equal + guard length1 == length2 else { + return false + } + if length1 > 0 { return d1.withUnsafeBytes { (b1: UnsafeRawBufferPointer) in return d2.withUnsafeBytes { (b2: UnsafeRawBufferPointer) in @@ -2732,12 +2733,12 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect let b1Address = b1.baseAddress! let b2Address = b2.baseAddress! - if b1Address == b2Address { + guard b1Address != b2Address else { return true - } else { - // Compare the contents - return memcmp(b1.baseAddress!, b2.baseAddress!, b2.count) == 0 } + + // Compare the contents + return memcmp(b1Address, b2Address, b2.count) == 0 } } }