Skip to content

[StdLib][RFC][DNM] Add isIdentical Methods for Quick Comparisons to String and Substring #82055

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions benchmark/single-source/StringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public var benchmarks: [BenchmarkInfo] {
runFunction: run_StringHasSuffixUnicode,
tags: [.validation, .api, .String],
legacyFactor: 1000),
BenchmarkInfo(
name: "StringIdentical",
runFunction: run_StringIdentical,
tags: [.validation, .api, .String]),
]

if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
Expand Down Expand Up @@ -1676,3 +1680,13 @@ public func run_iterateWords(_ n: Int) {
blackHole(swiftOrgHTML._words)
}
}

public func run_StringIdentical(_ n: Int) {
let str1 = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. "
let str2 = str1
for _ in 0 ..< n {
for _ in 0 ..< 100_000 {
check(str1.isIdentical(to: str2))
}
}
}
9 changes: 9 additions & 0 deletions benchmark/single-source/SubstringTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public let benchmarks = [
BenchmarkInfo(name: "SubstringFromLongString2", runFunction: run_SubstringFromLongString, tags: [.validation, .api, .String]),
BenchmarkInfo(name: "SubstringFromLongStringGeneric2", runFunction: run_SubstringFromLongStringGeneric, tags: [.validation, .api, .String]),
BenchmarkInfo(name: "SubstringTrimmingASCIIWhitespace", runFunction: run_SubstringTrimmingASCIIWhitespace, tags: [.validation, .api, .String]),
BenchmarkInfo(name: "SubstringIdentical", runFunction: run_SubstringIdentical, tags: [.validation, .String]),
]

// A string that doesn't fit in small string storage and doesn't fit in Latin-1
Expand Down Expand Up @@ -332,3 +333,11 @@ public func run _LessSubstringSubstringGenericStringProtocol(_ n: Int) {
}
}
*/

@inline(never)
public func run_SubstringIdentical(_ n: Int) {
let (a, b) = (ss1, ss1)
for _ in 1...n*500 {
blackHole(a.isIdentical(to: b))
}
}
20 changes: 19 additions & 1 deletion stdlib/public/core/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1112,4 +1112,22 @@ extension String {
}
}


extension String {
/// Returns a boolean value indicating whether this string is identical to
/// `other`.
///
/// Two string values are identical if there is no way to distinguish between
/// them.
///
/// Comparing strings this way includes comparing (normally) hidden
/// implementation details such as the memory location of any underlying
/// string storage object. Therefore, identical strings are guaranteed to
/// compare equal with `==`, but not all equal strings are considered
/// identical.
///
/// - Performance: O(1)
@_alwaysEmitIntoClient
public func isIdentical(to other: Self) -> Bool {
self._guts.rawBits == other._guts.rawBits
}
}
21 changes: 21 additions & 0 deletions stdlib/public/core/Substring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1307,3 +1307,24 @@ extension Substring {
return Substring(_unchecked: Slice(base: base, bounds: r))
}
}

extension Substring {
/// Returns a boolean value indicating whether this substring is identical to
/// `other`.
///
/// Two substring values are identical if there is no way to distinguish
/// between them.
///
/// Comparing substrings this way includes comparing (normally) hidden
/// implementation details such as the memory location of any underlying
/// substring storage object. Therefore, identical substrings are guaranteed
/// to compare equal with `==`, but not all equal substrings are considered
/// identical.
///
/// - Performance: O(1)
@_alwaysEmitIntoClient
public func isIdentical(to other: Self) -> Bool {
self._wholeGuts.rawBits == other._wholeGuts.rawBits &&
self._offsetRange == other._offsetRange
}
}
37 changes: 37 additions & 0 deletions test/stdlib/StringAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,41 @@ StringTests.test("hasPrefix/hasSuffix vs Character boundaries") {
expectFalse(s2.hasSuffix("\n"))
}

StringTests.test("isIdentical(to:) small ascii") {
let a = "Hello"
let b = "Hello"

precondition(a == b)

expectTrue(a.isIdentical(to: a))
expectTrue(b.isIdentical(to: b))
expectTrue(a.isIdentical(to: b)) // Both small ASCII strings
expectTrue(b.isIdentical(to: a))
}

StringTests.test("isIdentical(to:) small unicode") {
let a = "Cafe\u{301}"
let b = "Cafe\u{301}"
let c = "Café"

precondition(a == b)
precondition(b == c)

expectTrue(a.isIdentical(to: b))
expectTrue(b.isIdentical(to: a))
expectFalse(a.isIdentical(to: c))
expectFalse(b.isIdentical(to: c))
}

StringTests.test("isIdentical(to:) large ascii") {
let a = String(repeating: "foo", count: 1000)
let b = String(repeating: "foo", count: 1000)

precondition(a == b)

expectFalse(a.isIdentical(to: b)) // Two large, distinct native strings
expectTrue(a.isIdentical(to: a))
expectTrue(b.isIdentical(to: b))
}

runAllTests()
132 changes: 132 additions & 0 deletions test/stdlib/subString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ func checkHasContiguousStorageSubstring(_ x: Substring.UTF8View) {
expectTrue(hasStorage)
}

fileprivate func slices(
_ s: String,
from: Int,
to: Int
) -> (
Substring,
Substring,
Substring
) {
let s1 = s[s.index(s.startIndex, offsetBy: from) ..<
s.index(s.startIndex, offsetBy: to)]
let s2 = s1[s1.startIndex..<s1.endIndex]
let s3 = s2[s1.startIndex..<s1.endIndex]
return (s1, s2, s3)
}

fileprivate func allNotEmpty(
_ s: Substring...
) -> Bool {
s.allSatisfy { $0.isEmpty == false }
}

fileprivate func allEqual(
_ s: Substring...
) -> Bool {
for i in 0..<s.count {
for j in (i + 1)..<s.count {
if s[i] != s[j] {
return false
}
}
}
return true
}

SubstringTests.test("Equality") {
let s = "abcdefg"
let s1 = s[s.index(s.startIndex, offsetBy: 2) ..<
Expand Down Expand Up @@ -282,4 +317,101 @@ SubstringTests.test("Substring.base") {
}
}

SubstringTests.test("isIdentical(to:) small ascii") {
let (a1, a2, a3) = slices("Hello", from: 2, to: 4)
let (b1, b2, b3) = slices("Hello", from: 2, to: 4)

precondition(allNotEmpty(a1, a2, a3, b1, b2, b3))
precondition(allEqual(a1, a2, a3, b1, b2, b3))

expectTrue(a1.isIdentical(to: a1))
expectTrue(a1.isIdentical(to: a2))
expectTrue(a1.isIdentical(to: a3))
expectTrue(a1.isIdentical(to: b1))
expectTrue(a1.isIdentical(to: b2))
expectTrue(a1.isIdentical(to: b3))

expectTrue(a2.isIdentical(to: a1))
expectTrue(a2.isIdentical(to: a2))
expectTrue(a2.isIdentical(to: a3))
expectTrue(a2.isIdentical(to: b1))
expectTrue(a2.isIdentical(to: b2))
expectTrue(a2.isIdentical(to: b3))

expectTrue(a3.isIdentical(to: a1))
expectTrue(a3.isIdentical(to: a2))
expectTrue(a3.isIdentical(to: a3))
expectTrue(a3.isIdentical(to: b1))
expectTrue(a3.isIdentical(to: b2))
expectTrue(a3.isIdentical(to: b3))
}

SubstringTests.test("isIdentical(to:) small unicode") {
let (a1, a2, a3) = slices("Hello Cafe\u{301}", from: 2, to: 4)
let (b1, b2, b3) = slices("Hello Cafe\u{301}", from: 2, to: 4)
let (c1, c2, c3) = slices("Hello Café", from: 2, to: 4)

precondition(allNotEmpty(a1, a2, a3, b1, b2, b3, c1, c2, c3))
precondition(allEqual(a1, a2, a3, b1, b2, b3, c1, c2, c3))

expectTrue(a1.isIdentical(to: a1))
expectTrue(a1.isIdentical(to: a2))
expectTrue(a1.isIdentical(to: a3))
expectTrue(a1.isIdentical(to: b1))
expectTrue(a1.isIdentical(to: b2))
expectTrue(a1.isIdentical(to: b3))
expectFalse(a1.isIdentical(to: c1))
expectFalse(a1.isIdentical(to: c2))
expectFalse(a1.isIdentical(to: c3))

expectTrue(a2.isIdentical(to: a1))
expectTrue(a2.isIdentical(to: a2))
expectTrue(a2.isIdentical(to: a3))
expectTrue(a2.isIdentical(to: b1))
expectTrue(a2.isIdentical(to: b2))
expectTrue(a2.isIdentical(to: b3))
expectFalse(a2.isIdentical(to: c1))
expectFalse(a2.isIdentical(to: c2))
expectFalse(a2.isIdentical(to: c3))

expectTrue(a3.isIdentical(to: a1))
expectTrue(a3.isIdentical(to: a2))
expectTrue(a3.isIdentical(to: a3))
expectTrue(a3.isIdentical(to: b1))
expectTrue(a3.isIdentical(to: b2))
expectTrue(a3.isIdentical(to: b3))
expectFalse(a3.isIdentical(to: c1))
expectFalse(a3.isIdentical(to: c2))
expectFalse(a3.isIdentical(to: c3))
}

SubstringTests.test("isIdentical(to:) large ascii") {
let (a1, a2, a3) = slices(String(repeating: "Hello", count: 1000), from: 2, to: 4)
let (b1, b2, b3) = slices(String(repeating: "Hello", count: 1000), from: 2, to: 4)

precondition(allNotEmpty(a1, a2, a3, b1, b2, b3))
precondition(allEqual(a1, a2, a3, b1, b2, b3))

expectTrue(a1.isIdentical(to: a1))
expectTrue(a1.isIdentical(to: a2))
expectTrue(a1.isIdentical(to: a3))
expectFalse(a1.isIdentical(to: b1))
expectFalse(a1.isIdentical(to: b2))
expectFalse(a1.isIdentical(to: b3))

expectTrue(a2.isIdentical(to: a1))
expectTrue(a2.isIdentical(to: a2))
expectTrue(a2.isIdentical(to: a3))
expectFalse(a2.isIdentical(to: b1))
expectFalse(a2.isIdentical(to: b2))
expectFalse(a2.isIdentical(to: b3))

expectTrue(a3.isIdentical(to: a1))
expectTrue(a3.isIdentical(to: a2))
expectTrue(a3.isIdentical(to: a3))
expectFalse(a3.isIdentical(to: b1))
expectFalse(a3.isIdentical(to: b2))
expectFalse(a3.isIdentical(to: b3))
}

runAllTests()