Skip to content

Commit 9f28451

Browse files
eecksteinairspeedswift
authored andcommitted
[4.2] Some string performance changes (#16542)
* stdlib: remove some @inlineables from String API functions. Beside the general goal to remove inlinable functions, this reduces code size and also improves performance for several benchmarks. The performance problem was that by inlining top-level String API functions into client code (like String.count) it ended up calling non-inlinable internal String functions eventually. This is much slower than to make a single call at the top-level API boundary into the library. Inside the library all the internal String functions can be specialized and inlined. rdar://problem/39921548 * stdlib: fix performance regression for long string appends. Re-wrote the inner memcpy loops so that they can be vectorized. Also added a few inline(__always). Since we removed some @inlineable attributes this string-append code is not code generated in the client anymore. The code generation in the stdlib binary is different because all the precondition checks are not folded away. Using explicit loop control statements instead of for-in-range removes the precondition-overhead for those time critical memcpy loops. * stdlib: Speed up UTF8View -> Array conversion by using _copyContents * [test] Update diagnostic test for SR-7599
1 parent dfed20c commit 9f28451

File tree

9 files changed

+45
-31
lines changed

9 files changed

+45
-31
lines changed

stdlib/public/core/ContiguousArrayBuffer.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -642,15 +642,17 @@ internal func _copyCollectionToContiguousArray<
642642
_uninitializedCount: count,
643643
minimumCapacity: 0)
644644

645-
var p = result.firstElementAddress
646-
var i = source.startIndex
647-
for _ in 0..<count {
648-
// FIXME(performance): use _copyContents(initializing:).
649-
p.initialize(to: source[i])
650-
source.formIndex(after: &i)
651-
p += 1
652-
}
653-
_expectEnd(of: source, is: i)
645+
var p = UnsafeMutableBufferPointer(start: result.firstElementAddress, count: count)
646+
var (itr, end) = source._copyContents(initializing: p)
647+
648+
_debugPrecondition(itr.next() == nil,
649+
"invalid Collection: more than 'count' elements in collection")
650+
// We also have to check the evil shrink case in release builds, because
651+
// it can result in uninitialized array elements and therefore undefined
652+
// behavior.
653+
_precondition(end == p.endIndex,
654+
"invalid Collection: less than 'count' elements in collection")
655+
654656
return ContiguousArray(_buffer: result)
655657
}
656658

stdlib/public/core/StringGuts.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,6 @@ extension _StringGuts {
10301030
self.append(other._wholeString._guts, range: other._encodedOffsetRange)
10311031
}
10321032

1033-
@inlinable
10341033
public // TODO(StringGuts): for testing only
10351034
mutating func append(_ other: _StringGuts) {
10361035
// FIXME(TODO: JIRA): shouldn't _isEmptySingleton be sufficient?

stdlib/public/core/StringIndexConversions.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ extension String.Index {
4949
/// `sourcePosition` must be a valid index of at least one of the views
5050
/// of `target`.
5151
/// - target: The string referenced by the resulting index.
52-
@inlinable // FIXME(sil-serialize-all)
5352
public init?(
5453
_ sourcePosition: String.Index,
5554
within target: String

stdlib/public/core/StringLegacy.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ extension StringProtocol {
160160
}
161161

162162
extension String {
163-
@inlinable // FIXME(sil-serialize-all)
164163
public func hasPrefix(_ prefix: String) -> Bool {
165164
let prefixCount = prefix._guts.count
166165
if prefixCount == 0 { return true }
@@ -208,7 +207,6 @@ extension String {
208207
return self.starts(with: prefix)
209208
}
210209

211-
@inlinable // FIXME(sil-serialize-all)
212210
public func hasSuffix(_ suffix: String) -> Bool {
213211
let suffixCount = suffix._guts.count
214212
if suffixCount == 0 { return true }

stdlib/public/core/StringRangeReplaceableCollection.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ extension String : StringProtocol, RangeReplaceableCollection {
8989
@inlinable // FIXME(sil-serialize-all)
9090
public var endIndex: Index { return Index(encodedOffset: _guts.count) }
9191

92+
/// The number of characters in a string.
93+
public var count: Int {
94+
return distance(from: startIndex, to: endIndex)
95+
}
96+
9297
@inlinable
9398
@inline(__always)
9499
internal func _boundsCheck(_ index: Index) {
@@ -114,7 +119,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
114119
"String index range is out of bounds")
115120
}
116121

117-
@inlinable // FIXME(sil-serialize-all)
118122
internal func _index(atEncodedOffset offset: Int) -> Index {
119123
return _visitGuts(_guts, args: offset,
120124
ascii: { ascii, offset in return ascii.characterIndex(atOffset: offset) },
@@ -128,7 +132,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
128132
/// - Parameter i: A valid index of the collection. `i` must be less than
129133
/// `endIndex`.
130134
/// - Returns: The index value immediately after `i`.
131-
@inlinable // FIXME(sil-serialize-all)
132135
public func index(after i: Index) -> Index {
133136
return _visitGuts(_guts, args: i,
134137
ascii: { ascii, i in ascii.characterIndex(after: i) },
@@ -141,7 +144,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
141144
/// - Parameter i: A valid index of the collection. `i` must be greater than
142145
/// `startIndex`.
143146
/// - Returns: The index value immediately before `i`.
144-
@inlinable // FIXME(sil-serialize-all)
145147
public func index(before i: Index) -> Index {
146148
return _visitGuts(_guts, args: i,
147149
ascii: { ascii, i in ascii.characterIndex(before: i) },
@@ -171,7 +173,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
171173
/// to `index(before:)`.
172174
///
173175
/// - Complexity: O(*n*), where *n* is the absolute value of `n`.
174-
@inlinable // FIXME(sil-serialize-all)
175176
public func index(_ i: Index, offsetBy n: IndexDistance) -> Index {
176177
return _visitGuts(_guts, args: (i, n),
177178
ascii: { ascii, args in let (i, n) = args
@@ -219,7 +220,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
219220
/// the method returns `nil`.
220221
///
221222
/// - Complexity: O(*n*), where *n* is the absolute value of `n`.
222-
@inlinable // FIXME(sil-serialize-all)
223223
public func index(
224224
_ i: Index, offsetBy n: IndexDistance, limitedBy limit: Index
225225
) -> Index? {
@@ -241,7 +241,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
241241
/// - Returns: The distance between `start` and `end`.
242242
///
243243
/// - Complexity: O(*n*), where *n* is the resulting distance.
244-
@inlinable // FIXME(sil-serialize-all)
245244
public func distance(from start: Index, to end: Index) -> IndexDistance {
246245
return _visitGuts(_guts, args: (start, end),
247246
ascii: { ascii, args in let (start, end) = args
@@ -267,7 +266,6 @@ extension String : StringProtocol, RangeReplaceableCollection {
267266
///
268267
/// - Parameter i: A valid index of the string. `i` must be less than the
269268
/// string's end index.
270-
@inlinable // FIXME(sil-serialize-all)
271269
public subscript(i: Index) -> Character {
272270
return _visitGuts(_guts, args: i,
273271
ascii: { ascii, i in return ascii.character(at: i) },

stdlib/public/core/StringStorage.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ extension _SwiftStringStorage {
174174
@inlinable
175175
@nonobjc
176176
var unusedBuffer: UnsafeMutableBufferPointer<CodeUnit> {
177-
return UnsafeMutableBufferPointer(start: end, count: capacity - count)
177+
@inline(__always)
178+
get {
179+
return UnsafeMutableBufferPointer(start: end, count: capacity - count)
180+
}
178181
}
179182

180183
@inlinable

stdlib/public/core/UnmanagedString.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal typealias _UnmanagedASCIIString = _UnmanagedString<UInt8>
1616
internal typealias _UnmanagedUTF16String = _UnmanagedString<UTF16.CodeUnit>
1717

1818
@inlinable
19+
@inline(__always)
1920
internal
2021
func memcpy_zext<
2122
Target: FixedWidthInteger & UnsignedInteger,
@@ -24,12 +25,18 @@ func memcpy_zext<
2425
dst: UnsafeMutablePointer<Target>, src: UnsafePointer<Source>, count: Int
2526
) {
2627
_sanityCheck(Source.bitWidth < Target.bitWidth)
27-
for i in 0..<count {
28+
_sanityCheck(count >= 0)
29+
// Don't use the for-in-range syntax to avoid precondition checking in Range.
30+
// This enables vectorization of the memcpy loop.
31+
var i = 0
32+
while i < count {
2833
dst[i] = Target(src[i])
34+
i = i &+ 1
2935
}
3036
}
3137

3238
@inlinable
39+
@inline(__always)
3340
internal
3441
func memcpy_trunc<
3542
Target: FixedWidthInteger & UnsignedInteger,
@@ -38,8 +45,13 @@ func memcpy_trunc<
3845
dst: UnsafeMutablePointer<Target>, src: UnsafePointer<Source>, count: Int
3946
) {
4047
_sanityCheck(Source.bitWidth > Target.bitWidth)
41-
for i in 0..<count {
48+
_sanityCheck(count >= 0)
49+
// Don't use the for-in-range syntax to avoid precondition checking in Range.
50+
// This enables vectorization of the memcpy loop.
51+
var i = 0
52+
while i < count {
4253
dst[i] = Target(truncatingIfNeeded: src[i])
54+
i = i &+ 1
4355
}
4456
}
4557

@@ -194,6 +206,7 @@ extension _UnmanagedString : _StringVariant {
194206
}
195207

196208
@inlinable // FIXME(sil-serialize-all)
209+
@inline(__always)
197210
internal func _copy<TargetCodeUnit>(
198211
into target: UnsafeMutableBufferPointer<TargetCodeUnit>
199212
) where TargetCodeUnit : FixedWidthInteger & UnsignedInteger {

test/Constraints/diagnostics.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ func rdar20142523() {
161161
// <rdar://problem/21080030> Bad diagnostic for invalid method call in boolean expression: (_, ExpressibleByIntegerLiteral)' is not convertible to 'ExpressibleByIntegerLiteral
162162
func rdar21080030() {
163163
var s = "Hello"
164-
if s.count() == 0 {} // expected-error{{cannot call value of non-function type 'Int'}}{{13-15=}}
164+
// SR-7599: This should be `cannot_call_non_function_value`
165+
if s.count() == 0 {} // expected-error{{cannot invoke 'count' with no arguments}}
165166
}
166167

167168
// <rdar://problem/21248136> QoI: problem with return type inference mis-diagnosed as invalid arguments

validation-test/stdlib/Arrays.swift.gyb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ CopyToNativeArrayBufferTests.test("Collection._copyToContiguousArray()") {
6868
expectEqual(0, c.timesMakeIteratorCalled.value)
6969
expectEqual(0, c.timesStartIndexCalled.value)
7070
let copy = c._copyToContiguousArray()
71-
expectEqual(0, c.timesMakeIteratorCalled.value)
71+
// _copyToContiguousArray calls Sequence._copyContents, which makes an iterator.
72+
expectEqual(1, c.timesMakeIteratorCalled.value)
7273
expectNotEqual(0, c.timesStartIndexCalled.value)
7374
expectEqualSequence(
7475
Array(10..<27),
@@ -82,7 +83,8 @@ CopyToNativeArrayBufferTests.test("Collection._copyToContiguousArray()") {
8283
expectEqual(0, wrapped.timesMakeIteratorCalled.value)
8384
expectEqual(0, wrapped.timesStartIndexCalled.value)
8485
let copy = s._copyToContiguousArray()
85-
expectEqual(0, wrapped.timesMakeIteratorCalled.value)
86+
// _copyToContiguousArray calls Sequence._copyContents, which makes an iterator.
87+
expectEqual(1, wrapped.timesMakeIteratorCalled.value)
8688
expectNotEqual(0, wrapped.timesStartIndexCalled.value)
8789

8890
expectEqualSequence(
@@ -2418,10 +2420,9 @@ for (step, evilBoundsCheck) in [ (1, true), (-1, false), (-1, true) ] {
24182420
? evilBoundsError
24192421
: "invalid Collection: count differed in successive traversals"
24202422

2421-
let constructionMessage =
2422-
/*_isStdlibInternalChecksEnabled() && !evilBoundsCheck && step <= 0
2423-
? "_UnsafePartiallyInitializedContiguousArrayBuffer has no more capacity"
2424-
:*/ message
2423+
let constructionMessage = step < 0
2424+
? "invalid Collection: less than 'count' elements in collection"
2425+
: "invalid Collection: more than 'count' elements in collection"
24252426

24262427
// The invalid Collection error is a _debugPreconditon that will only fire
24272428
// in a Debug assert configuration.
@@ -2472,7 +2473,7 @@ for (step, evilBoundsCheck) in [ (1, true), (-1, false), (-1, true) ] {
24722473
.code {
24732474
let evil = EvilCollection(step, boundsChecked: evilBoundsCheck)
24742475

2475-
if expectedToFail {
2476+
if step < 0 || _isDebugAssertConfiguration() {
24762477
expectCrashLater()
24772478
}
24782479

0 commit comments

Comments
 (0)