Skip to content

Commit a061df5

Browse files
milsemanairspeedswift
authored andcommitted
[5.0] [String] Comparison Speedups (#20984)
* [String] Fix corner case in comparison fast-path. (#20937) When in a post-binary-prefix-scan fast-path, we need to make sure we are comparing a full-segment scalar, otherwise we miss situations where a combining end-of-segment scalar would be reordered with a prior combining scalar in the same segment under normalization in one string but not the other. This was hidden by the fact that many combining scalars are not NFC_QC=maybe, but those which are not present in any precomposed form have NFC_QC=yes. Added tests. * [String] Normalization-boundary-before UTF-8 fast path. All latiny (<0x300) scalars have normalization boundaries before them, so when asking if memory containing a scalar encoded in valid UTF-8 has a boundary before it, check if the leading byte definitely encodes a scalar less than 0x300. * [String] Refactor and fast-path normalization Refactor some normalization queries into StringNormalization.swift, and add more latiny (<0x300) fast-paths. * [String] Speed up constant factors on comparison. Include some tuning and tweaking to reduce the constant factors involved in string comparison. This yields considerable improvement on our micro-benchmarks, and allows us to make less inlinable code and have a smaller ABI surface area. Adds more extensive testing of corner cases in our existing fast-paths. * [String] Hand-increment loop variable for perf. Hand-incrementing the loop variable allows us to skip overflow detection, and will permit more vectorization improvements in the future. For now, it gives us perf improvements in nano-benchmarks.
1 parent fd1ef3d commit a061df5

File tree

7 files changed

+335
-264
lines changed

7 files changed

+335
-264
lines changed

stdlib/public/core/NormalizedCodeUnitIterator.swift

Lines changed: 16 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -16,58 +16,6 @@ extension _Normalization {
1616
internal typealias _SegmentOutputBuffer = _FixedArray16<UInt16>
1717
}
1818

19-
extension Unicode.Scalar {
20-
// Normalization boundary - a place in a string where everything left of the
21-
// boundary can be normalized independently from everything right of the
22-
// boundary. The concatenation of each result is the same as if the entire
23-
// string had been normalized as a whole.
24-
//
25-
// Normalization segment - a sequence of code units between two normalization
26-
// boundaries (without any boundaries in the middle). Note that normalization
27-
// segments can, as a process of normalization, expand, contract, and even
28-
// produce new sub-segments.
29-
30-
// Whether this scalar value always has a normalization boundary before it.
31-
internal var _hasNormalizationBoundaryBefore: Bool {
32-
_internalInvariant(Int32(exactly: self.value) != nil, "top bit shouldn't be set")
33-
let value = Int32(bitPattern: self.value)
34-
return 0 != __swift_stdlib_unorm2_hasBoundaryBefore(
35-
_Normalization._nfcNormalizer, value)
36-
}
37-
internal var _isNFCQCYes: Bool {
38-
return __swift_stdlib_u_getIntPropertyValue(
39-
Builtin.reinterpretCast(value), __swift_stdlib_UCHAR_NFC_QUICK_CHECK
40-
) == 1
41-
}
42-
}
43-
44-
internal func _tryNormalize(
45-
_ input: UnsafeBufferPointer<UInt16>,
46-
into outputBuffer:
47-
UnsafeMutablePointer<_Normalization._SegmentOutputBuffer>
48-
) -> Int? {
49-
return _tryNormalize(input, into: _castOutputBuffer(outputBuffer))
50-
}
51-
internal func _tryNormalize(
52-
_ input: UnsafeBufferPointer<UInt16>,
53-
into outputBuffer: UnsafeMutableBufferPointer<UInt16>
54-
) -> Int? {
55-
var err = __swift_stdlib_U_ZERO_ERROR
56-
let count = __swift_stdlib_unorm2_normalize(
57-
_Normalization._nfcNormalizer,
58-
input.baseAddress._unsafelyUnwrappedUnchecked,
59-
numericCast(input.count),
60-
outputBuffer.baseAddress._unsafelyUnwrappedUnchecked,
61-
numericCast(outputBuffer.count),
62-
&err
63-
)
64-
guard err.isSuccess else {
65-
// The output buffer needs to grow
66-
return nil
67-
}
68-
return numericCast(count)
69-
}
70-
7119
//
7220
// Pointer casting helpers
7321
//
@@ -131,9 +79,11 @@ extension UnsafeBufferPointer where Element == UInt8 {
13179
if index == 0 || index == count {
13280
return true
13381
}
134-
13582
assert(!_isContinuation(self[_unchecked: index]))
13683

84+
// Sub-300 latiny fast-path
85+
if self[_unchecked: index] < 0xCC { return true }
86+
13787
let cu = _decodeScalar(self, startingAt: index).0
13888
return cu._hasNormalizationBoundaryBefore
13989
}
@@ -191,34 +141,6 @@ internal struct _NormalizedUTF8CodeUnitIterator: IteratorProtocol {
191141

192142
return utf8Buffer[bufferIndex]
193143
}
194-
195-
internal mutating func compare(
196-
with other: _NormalizedUTF8CodeUnitIterator
197-
) -> _StringComparisonResult {
198-
var mutableOther = other
199-
200-
for cu in self {
201-
if let otherCU = mutableOther.next() {
202-
let result = _lexicographicalCompare(cu, otherCU)
203-
if result == .equal {
204-
continue
205-
} else {
206-
return result
207-
}
208-
} else {
209-
//other returned nil, we are greater
210-
return .greater
211-
}
212-
}
213-
214-
//we ran out of code units, either we are equal, or only we ran out and
215-
//other is greater
216-
if let _ = mutableOther.next() {
217-
return .less
218-
} else {
219-
return .equal
220-
}
221-
}
222144
}
223145

224146
extension _NormalizedUTF8CodeUnitIterator: Sequence { }
@@ -513,16 +435,6 @@ extension _NormalizedUTF8CodeUnitIterator_2 {
513435
@inline(__always)
514436
@_effects(releasenone)
515437
private mutating func fastPathFill() -> (numRead: Int, numWritten: Int) {
516-
// Quick check if a scalar is NFC and a segment starter
517-
@inline(__always) func isNFCStarter(_ scalar: Unicode.Scalar) -> Bool {
518-
// Fast-path: All scalars up through U+02FF are NFC and have boundaries
519-
// before them
520-
if scalar.value < 0x300 { return true }
521-
522-
// Otherwise, consult the properties
523-
return scalar._hasNormalizationBoundaryBefore && scalar._isNFCQCYes
524-
}
525-
526438
// TODO: Additional fast-path: All CCC-ascending NFC_QC segments are NFC
527439
// TODO: Just freakin do normalization and don't bother with ICU
528440
var outputCount = 0
@@ -540,7 +452,7 @@ extension _NormalizedUTF8CodeUnitIterator_2 {
540452

541453
if _slowPath(
542454
!utf8.hasNormalizationBoundary(before: inputCount &+ len)
543-
|| !isNFCStarter(scalar)
455+
|| !scalar._isNFCStarter
544456
) {
545457
break
546458
}
@@ -566,7 +478,7 @@ extension _NormalizedUTF8CodeUnitIterator_2 {
566478
if _slowPath(
567479
!gutsSlice.foreignHasNormalizationBoundary(
568480
before: startIdx.encoded(offsetBy: len))
569-
|| !isNFCStarter(scalar)
481+
|| !scalar._isNFCStarter
570482
) {
571483
break
572484
}
@@ -627,29 +539,22 @@ extension _NormalizedUTF8CodeUnitIterator_2 {
627539

628540
@_effects(readonly)
629541
internal mutating func compare(
630-
with other: _NormalizedUTF8CodeUnitIterator_2
631-
) -> _StringComparisonResult {
632-
var iter = self
542+
with other: _NormalizedUTF8CodeUnitIterator_2,
543+
expecting: _StringComparisonResult
544+
) -> Bool {
633545
var mutableOther = other
634546

635-
while let cu = iter.next() {
636-
if let otherCU = mutableOther.next() {
637-
let result = _lexicographicalCompare(cu, otherCU)
638-
if result == .equal {
639-
continue
640-
}
641-
return result
547+
for cu in self {
548+
guard let otherCU = mutableOther.next() else {
549+
// We have more code units, therefore we are greater
550+
return false
642551
}
643-
//other returned nil, we are greater
644-
return .greater
552+
if cu == otherCU { continue }
553+
return expecting == .less ? cu < otherCU : false
645554
}
646555

647-
//we ran out of code units, either we are equal, or only we ran out and
648-
//other is greater
649-
if let _ = mutableOther.next() {
650-
return .less
651-
}
652-
return .equal
556+
// We have exhausted our code units. We are less if there's more remaining
557+
return mutableOther.next() == nil ? expecting == .equal : expecting == .less
653558
}
654559
}
655560

0 commit comments

Comments
 (0)