Skip to content

Commit 7a8ba2d

Browse files
committed
stdlib: Improve String comparison performance
This patch improves String comparison performance by introducing a memcmp-like helper routine that will perform a binary comparison on two buffers of equal length N and return the offset of the first difference if any, or N if they contain no differences. The helper is invoked from String operators == (for non-Objective-C runtimes only) and < (for all runtimes) when possible, the critera being that the Strings must have the same element size and must use contiguous storage. If we detect a mismatch in the binary representation of the Strings we fall back to using the ICU routines to determine the result of the comparison. Strings of differing binary length are handled by invoking the helper to check up to the length of the shorter string and falling back to ICU to deal with the remainder. As a special case for UTF16 Strings, before falling back to ICU to handle mismatched sub-strings, we take care to insure that we don't pass sub-strings that begin in the middle of a collation element.
1 parent dc16a94 commit 7a8ba2d

File tree

5 files changed

+244
-10
lines changed

5 files changed

+244
-10
lines changed

stdlib/public/SwiftShims/UnicodeShims.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ _swift_stdlib_unicode_compare_utf8_utf8(const unsigned char *Left,
8383
const unsigned char *Right,
8484
__swift_int32_t RightLength);
8585

86+
SWIFT_RUNTIME_STDLIB_INTERFACE
87+
__attribute__((__pure__)) __swift_int32_t
88+
_swift_stdlib_unicode_find_longest_contraction(void);
89+
8690
SWIFT_RUNTIME_STDLIB_INTERFACE
8791
void *_swift_stdlib_unicodeCollationIterator_create(
8892
const __swift_uint16_t *Str,

stdlib/public/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ set(SWIFTLIB_ESSENTIAL
119119
StringComparable.swift
120120
StringCore.swift
121121
StringHashable.swift
122+
StringHelpers.cpp
122123
StringInterpolation.swift
123124
StringLegacy.swift
124125
StringRangeReplaceableCollection.swift.gyb

stdlib/public/core/StringComparable.swift

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ public func _stdlib_compareNSStringDeterministicUnicodeCollationPointer(
3838
) -> Int32
3939
#endif
4040

41+
@_silgen_name("_swift_string_memcmp")
42+
func _swift_string_memcmp(
43+
_ s1: UnsafeMutableRawPointer,
44+
_ s2: UnsafeMutableRawPointer,
45+
_ n: Int
46+
) -> Int
47+
4148
extension String {
4249
#if _runtime(_ObjC)
4350
/// This is consistent with Foundation, but incorrect as defined by Unicode.
@@ -65,11 +72,67 @@ extension String {
6572
}
6673
#endif
6774

68-
/// Compares two strings with the Unicode Collation Algorithm.
75+
private
76+
func _compareCodeUnitsASCII(_ rhs: String) -> Int {
77+
let n = min(_core.count, rhs._core.count)
78+
let selfStart = UnsafeMutableRawPointer(_core.startASCII)
79+
let rhsStart = UnsafeMutableRawPointer(rhs._core.startASCII)
80+
let firstDiff = _swift_string_memcmp(selfStart, rhsStart, n)
81+
if _core.count == rhs._core.count && firstDiff == n {
82+
return 0
83+
}
84+
return _compareString(rhs, offset: firstDiff)
85+
}
86+
6987
@inline(never)
70-
@_semantics("stdlib_binary_only") // Hide the CF/ICU dependency
88+
@_semantics("stdlib_binary_only") // Hide the ICU dependency
89+
private
90+
func _compareCodeUnitsUTF16(_ rhs: String) -> Int {
91+
let n = min(_core.count, rhs._core.count) << _core.elementShift
92+
let selfStart = UnsafeMutableRawPointer(_core.startUTF16)
93+
let rhsStart = UnsafeMutableRawPointer(rhs._core.startUTF16)
94+
var firstDiff = _swift_string_memcmp(selfStart, rhsStart, n)
95+
if _core.count == rhs._core.count && firstDiff == n {
96+
return 0
97+
}
98+
// At this point we have to fall back to the UCA.
99+
// In order to properly order contractions and surrogate pairs we can't
100+
// invoke the UCA with UTF16 strings that start in the middle of a contraction
101+
// or surrogate pair. Rather than carry out a lot of expensive operations to
102+
// figure out if we're in the middle of a contraction or surrogate pair, we
103+
// simply step back a fixed number of code units, equal to the longest
104+
// possible contraction, or the length of a surrogate pair (2), whichever is
105+
// greater, minus 1 (while taking care that we don't step back past the start
106+
// of the strings).
107+
// This will produce a correct result at the cost of re-comparing a few
108+
// characters that we know are equal, which is likely much cheaper than
109+
// calculating a more precise number of code units to step back.
110+
firstDiff = firstDiff >> _core.elementShift
111+
let surrogateLength = 2
112+
let stepBack = max(Int(_swift_stdlib_unicode_find_longest_contraction()), surrogateLength) - 1
113+
firstDiff = firstDiff >= stepBack ? firstDiff - stepBack : 0
114+
return _compareString(rhs, offset: firstDiff)
115+
}
116+
117+
public // @testable
118+
func _compareCodeUnits(_ rhs: String) -> Int {
119+
if _core.isASCII == rhs._core.isASCII &&
120+
_core.hasContiguousStorage && rhs._core.hasContiguousStorage {
121+
return _core.isASCII ? _compareCodeUnitsASCII(rhs) : _compareCodeUnitsUTF16(rhs)
122+
}
123+
return _compareString(rhs)
124+
}
125+
126+
/// Compares two strings with the Unicode Collation Algorithm.
71127
public // @testable
72128
func _compareDeterministicUnicodeCollation(_ rhs: String) -> Int {
129+
return self._compareDeterministicUnicodeCollation(rhs, offset: 0)
130+
}
131+
132+
@inline(never)
133+
@_semantics("stdlib_binary_only") // Hide the CF/ICU dependency
134+
public
135+
func _compareDeterministicUnicodeCollation(_ rhs: String, offset: Int = 0) -> Int {
73136
// Note: this operation should be consistent with equality comparison of
74137
// Character.
75138
#if _runtime(_ObjC)
@@ -95,18 +158,18 @@ extension String {
95158
return -rhs._compareDeterministicUnicodeCollation(self)
96159
case (false, false):
97160
return Int(_swift_stdlib_unicode_compare_utf16_utf16(
98-
_core.startUTF16, Int32(_core.count),
99-
rhs._core.startUTF16, Int32(rhs._core.count)))
161+
_core.startUTF16 + offset, Int32(_core.count - offset),
162+
rhs._core.startUTF16 + offset, Int32(rhs._core.count - offset)))
100163
case (true, true):
101164
return Int(_swift_stdlib_unicode_compare_utf8_utf8(
102-
_core.startASCII, Int32(_core.count),
103-
rhs._core.startASCII, Int32(rhs._core.count)))
165+
_core.startASCII + offset, Int32(_core.count - offset),
166+
rhs._core.startASCII + offset, Int32(rhs._core.count - offset)))
104167
}
105168
#endif
106169
}
107170

108171
public // @testable
109-
func _compareString(_ rhs: String) -> Int {
172+
func _compareString(_ rhs: String, offset: Int = 0) -> Int {
110173
#if _runtime(_ObjC)
111174
// We only want to perform this optimization on objc runtimes. Elsewhere,
112175
// we will make it follow the unicode collation algorithm even for ASCII.
@@ -115,7 +178,7 @@ extension String {
115178
return _compareASCII(rhs)
116179
}
117180
#endif
118-
return _compareDeterministicUnicodeCollation(rhs)
181+
return _compareDeterministicUnicodeCollation(rhs, offset: offset)
119182
}
120183
}
121184

@@ -133,14 +196,16 @@ extension String : Equatable {
133196
lhs._core.startASCII, rhs._core.startASCII,
134197
rhs._core.count) == 0
135198
}
136-
#endif
137199
return lhs._compareString(rhs) == 0
200+
#else
201+
return lhs._compareCodeUnits(rhs) == 0
202+
#endif
138203
}
139204
}
140205

141206
extension String : Comparable {
142207
public static func < (lhs: String, rhs: String) -> Bool {
143-
return lhs._compareString(rhs) < 0
208+
return lhs._compareCodeUnits(rhs) < 0
144209
}
145210
}
146211

stdlib/public/core/StringHelpers.cpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//===-- StringHelpers.c - Optimized String helper routines ----------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
///
13+
/// \file
14+
/// This file contains optimized helper routines for various String operations.
15+
///
16+
//===----------------------------------------------------------------------===//
17+
18+
#include <cassert>
19+
#include "../SwiftShims/SwiftStddef.h"
20+
#include "../SwiftShims/SwiftStdint.h"
21+
22+
using wide_t = __swift_uintptr_t;
23+
using narrow_t = __swift_uint8_t;
24+
25+
union iterator_t {
26+
iterator_t(const void *v) : v(v) {}
27+
const wide_t *w;
28+
const narrow_t *b;
29+
const void *v;
30+
31+
const __swift_uintptr_t i;
32+
};
33+
34+
constexpr __swift_size_t wide_size = sizeof(wide_t);
35+
constexpr __swift_size_t min_wide_len = wide_size * 2 - 1;
36+
constexpr __swift_size_t wide_align_mask = wide_size - 1;
37+
38+
static_assert(sizeof(narrow_t) == 1, "Narrow type expected to be of size 1");
39+
40+
static
41+
__swift_size_t _swift_string_memcmp_narrow(iterator_t it1,
42+
iterator_t it2,
43+
__swift_size_t n) {
44+
__swift_size_t bytes_left = n;
45+
while (bytes_left > 0) {
46+
if (*it1.b != *it2.b)
47+
break;
48+
++it1.b;
49+
++it2.b;
50+
--bytes_left;
51+
}
52+
return n - bytes_left;
53+
}
54+
55+
static
56+
__swift_size_t _swift_string_memcmp_wide(iterator_t it1,
57+
iterator_t it2,
58+
__swift_size_t n) {
59+
// See below for why we expect at least this many bytes.
60+
assert(n >= (wide_size - 1 + wide_size) && "Too few bytes to compare");
61+
__swift_size_t bytes_left = n;
62+
// Alignment loop:
63+
// Doesn't check bytes_left, assumes caller supplied >= (wide_size - 1) bytes.
64+
while ((it1.i & wide_align_mask) != 0) {
65+
if (*it1.b != *it2.b)
66+
goto matchfail;
67+
++it1.b;
68+
++it2.b;
69+
--bytes_left;
70+
}
71+
// Wide compare loop:
72+
// Does at least one iteration, assumes that we have >= wide_size bytes
73+
// remaining after the alignment loop.
74+
assert((it1.i & wide_align_mask) == 0 && "Expecting first buffer to be aligned");
75+
assert((it2.i & wide_align_mask) == 0 && "Expecting second buffer to be aligned");
76+
do {
77+
if (*it1.w != *it2.w)
78+
goto matchfail;
79+
++it1.w;
80+
++it2.w;
81+
bytes_left -= wide_size;
82+
} while (bytes_left >= wide_size);
83+
// Residue loop:
84+
while (bytes_left > 0) {
85+
if (*it1.b != *it2.b)
86+
break;
87+
++it1.b;
88+
++it2.b;
89+
--bytes_left;
90+
}
91+
return n - bytes_left;
92+
93+
matchfail:
94+
// Residue loop for mismatched buffers:
95+
// We know the buffers contain bytes that don't match, so
96+
// we don't have to care about checking bytes_left.
97+
while (*it1.b == *it2.b) {
98+
++it1.b;
99+
++it2.b;
100+
--bytes_left;
101+
assert(bytes_left > 0 && "Expecting a mismatch prior to the end of the buffer");
102+
}
103+
return n - bytes_left;
104+
}
105+
106+
// Compares n bytes in s1 and s2, respectively, and returns the offset
107+
// to the first differing byte, or n if s1 is identical to s2.
108+
extern "C"
109+
__swift_size_t _swift_string_memcmp(const void *s1,
110+
const void *s2,
111+
__swift_size_t n) {
112+
iterator_t it1(s1), it2(s2);
113+
// If we want to operate on naturally aligned data we need both inputs
114+
// to be aligned -- failing that we want them to at least be misaligned
115+
// to the same degree so we can compare bytes until they're aligned.
116+
return n >= min_wide_len &&
117+
(it1.i & wide_align_mask) == (it2.i & wide_align_mask) ?
118+
_swift_string_memcmp_wide(it1, it2, n) :
119+
_swift_string_memcmp_narrow(it1, it2, n);
120+
}

stdlib/public/stubs/UnicodeNormalization.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,50 @@ swift::_swift_stdlib_unicode_compare_utf8_utf8(const unsigned char *LeftString,
196196
return Diff;
197197
}
198198

199+
/// Used by _swift_stdlib_unicode_find_longest_contraction below.
200+
static int32_t CachedLongestContraction = -1;
201+
202+
/// Finds and returns the longest contraction defined by the root collator.
203+
/// Results is the length of the longest contraction (in UChars, i.e. UTF16 code units).
204+
/// The result is cached in the global static CachedLongestContraction.
205+
int32_t
206+
swift::_swift_stdlib_unicode_find_longest_contraction(void) {
207+
// In order to play nice with other threads that enter this function
208+
// on SMP systems we copy CachedLongestContraction to a local and use
209+
// that during for calculations, only updating it once we're ready
210+
// to return to the caller. We don't need any sort of synchronization
211+
// because we expect this function to be idempotent.
212+
int32_t LocalLongestContraction = CachedLongestContraction;
213+
if (LocalLongestContraction >= 0)
214+
return LocalLongestContraction;
215+
USet *Contractions = uset_openEmpty();
216+
UErrorCode ErrorCode = U_ZERO_ERROR;
217+
if (!Contractions) {
218+
swift::crash("uset_openEmpty: Unable to create a new set.");
219+
}
220+
std::unique_ptr<USet, decltype(&uset_close)> ContractionsPtr(Contractions, uset_close);
221+
ucol_getContractionsAndExpansions(GetRootCollator(), Contractions, nullptr, FALSE, &ErrorCode);
222+
if (U_FAILURE(ErrorCode)) {
223+
swift::crash("ucol_getContractionsAndExpansions: Unable to get root collator's contractions.");
224+
}
225+
int32_t NumContractions = uset_getItemCount(Contractions);
226+
UChar32 Start, End;
227+
for (int32_t i = 0; i < NumContractions; ++i) {
228+
int32_t ItemLength = uset_getItem(Contractions, i, &Start, &End, nullptr, 0,
229+
&ErrorCode);
230+
assert(ItemLength > 0 && "Expecting the set of contractions to only contain strings, not ranges");
231+
if (ErrorCode == U_BUFFER_OVERFLOW_ERROR)
232+
ErrorCode = U_ZERO_ERROR;
233+
if (U_FAILURE(ErrorCode)) {
234+
swift::crash("uset_getItem: Unable to get item from set.");
235+
}
236+
if (ItemLength > LocalLongestContraction)
237+
LocalLongestContraction = ItemLength;
238+
}
239+
CachedLongestContraction = LocalLongestContraction;
240+
return LocalLongestContraction;
241+
}
242+
199243
void *swift::_swift_stdlib_unicodeCollationIterator_create(
200244
const __swift_uint16_t *Str, __swift_uint32_t Length) {
201245
UErrorCode ErrorCode = U_ZERO_ERROR;

0 commit comments

Comments
 (0)