Skip to content

Commit bc7be13

Browse files
committed
More efficient append(Substring), and fix use-after-free bug
1 parent 0fc297e commit bc7be13

File tree

6 files changed

+141
-93
lines changed

6 files changed

+141
-93
lines changed

stdlib/public/core/SmallString.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,18 @@ extension _SmallString {
243243
// Appending
244244
@usableFromInline // testable
245245
@_effects(releasenone)
246-
internal init?(base: _StringGuts, appending other: _StringGuts) {
246+
internal init?(
247+
base: __shared _StringGuts, appending other: __shared _StringGuts
248+
) {
249+
self.init(
250+
base: _SlicedStringGuts(base), appending: _SlicedStringGuts(other))
251+
}
252+
253+
// Appending
254+
@_effects(releasenone)
255+
internal init?(
256+
base: __shared _SlicedStringGuts, appending other: __shared _SlicedStringGuts
257+
) {
247258
guard (base.utf8Count + other.utf8Count) <= _SmallString.capacity else {
248259
return nil
249260
}

stdlib/public/core/StringGuts.swift

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ extension _StringGuts {
6565

6666
@inlinable @inline(__always)
6767
internal init(_ storage: _StringStorage) {
68-
// TODO(UTF8): We should probably store perf flags on the storage's capacity
6968
self.init(_StringObject(storage))
7069
}
7170

@@ -232,53 +231,17 @@ extension _StringGuts {
232231
// Contents of the buffer are unspecified if nil is returned.
233232
@inlinable
234233
internal func copyUTF8(into mbp: UnsafeMutableBufferPointer<UInt8>) -> Int? {
235-
let ptr = mbp.baseAddress._unsafelyUnwrappedUnchecked
236-
if _fastPath(self.isFastUTF8) {
237-
return self.withFastUTF8 { utf8 in
238-
guard utf8.count <= mbp.count else { return nil }
239-
240-
let utf8Start = utf8.baseAddress._unsafelyUnwrappedUnchecked
241-
ptr.initialize(from: utf8Start, count: utf8.count)
242-
return utf8.count
243-
}
244-
}
245-
246-
return _foreignCopyUTF8(into: mbp)
247-
}
248-
249-
@_effects(releasenone)
250-
@usableFromInline @inline(never) // slow-path
251-
internal func _foreignCopyUTF8(
252-
into mbp: UnsafeMutableBufferPointer<UInt8>
253-
) -> Int? {
254-
var ptr = mbp.baseAddress._unsafelyUnwrappedUnchecked
255-
var numWritten = 0
256-
for cu in String(self).utf8 {
257-
guard numWritten < mbp.count else { return nil }
258-
ptr.initialize(to: cu)
259-
ptr += 1
260-
numWritten += 1
261-
}
262-
263-
return numWritten
234+
// TODO(UTF8 perf): minor perf win by avoiding slicing if fast...
235+
return _SlicedStringGuts(self).copyUTF8(into: mbp)
264236
}
265237

266238
internal var utf8Count: Int {
267239
@inline(__always) get {
268-
// TODO: Might be worth burning a bit for count-is-UTF-8, regardless of
269-
// fast status.
270-
if _fastPath(self.isFastUTF8) {
271-
return self.count
272-
}
273-
return _foreignUTF8Count()
240+
if _fastPath(self.isFastUTF8) { return count }
241+
return _SlicedStringGuts(self).utf8Count
274242
}
275243
}
276244

277-
@_effects(releasenone)
278-
@inline(never) // slow-path
279-
internal func _foreignUTF8Count() -> Int {
280-
return String(self).utf8.count
281-
}
282245
}
283246

284247
// Index
@@ -329,11 +292,25 @@ internal struct _SlicedStringGuts {
329292
@inline(__always) get { return _guts.isNFCFastUTF8 }
330293
}
331294

295+
@inlinable
296+
internal var isASCII: Bool {
297+
@inline(__always) get { return _guts.isASCII }
298+
}
299+
332300
@inlinable
333301
internal var isFastUTF8: Bool {
334302
@inline(__always) get { return _guts.isFastUTF8 }
335303
}
336304

305+
internal var utf8Count: Int {
306+
@inline(__always) get {
307+
if _fastPath(self.isFastUTF8) {
308+
return _offsetRange.count
309+
}
310+
return Substring(self).utf8.count
311+
}
312+
}
313+
337314
@inlinable
338315
internal var range: Range<String.Index> {
339316
@inline(__always) get {
@@ -348,6 +325,39 @@ internal struct _SlicedStringGuts {
348325
) rethrows -> R {
349326
return try _guts.withFastUTF8(range: _offsetRange, f)
350327
}
351-
}
352328

329+
// Copy UTF-8 contents. Returns number written or nil if not enough space.
330+
// Contents of the buffer are unspecified if nil is returned.
331+
@inlinable
332+
internal func copyUTF8(into mbp: UnsafeMutableBufferPointer<UInt8>) -> Int? {
333+
let ptr = mbp.baseAddress._unsafelyUnwrappedUnchecked
334+
if _fastPath(self.isFastUTF8) {
335+
return self.withFastUTF8 { utf8 in
336+
guard utf8.count <= mbp.count else { return nil }
337+
338+
let utf8Start = utf8.baseAddress._unsafelyUnwrappedUnchecked
339+
ptr.initialize(from: utf8Start, count: utf8.count)
340+
return utf8.count
341+
}
342+
}
343+
344+
return _foreignCopyUTF8(into: mbp)
345+
}
353346

347+
@_effects(releasenone)
348+
@usableFromInline @inline(never) // slow-path
349+
internal func _foreignCopyUTF8(
350+
into mbp: UnsafeMutableBufferPointer<UInt8>
351+
) -> Int? {
352+
var ptr = mbp.baseAddress._unsafelyUnwrappedUnchecked
353+
var numWritten = 0
354+
for cu in Substring(self).utf8 {
355+
guard numWritten < mbp.count else { return nil }
356+
ptr.initialize(to: cu)
357+
ptr += 1
358+
numWritten += 1
359+
}
360+
361+
return numWritten
362+
}
363+
}

stdlib/public/core/StringGutsRangeReplaceable.swift

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ extension _StringGuts {
6464
}
6565

6666
// Grow to accomodate at least `n` code units
67-
internal mutating func grow(_ n: Int) {
67+
private mutating func grow(_ n: Int) {
6868
defer { self._invariantCheck() }
6969

7070
_sanityCheck(
@@ -79,7 +79,6 @@ extension _StringGuts {
7979
initializingFrom: $0, capacity: growthTarget, isASCII: isASCII)
8080
}
8181

82-
// TODO(UTF8): Track known ascii
8382
self = _StringGuts(storage)
8483
return
8584
}
@@ -88,7 +87,7 @@ extension _StringGuts {
8887
}
8988

9089
@inline(never) // slow-path
91-
internal mutating func _foreignGrow(_ n: Int) {
90+
private mutating func _foreignGrow(_ n: Int) {
9291
// TODO(UTF8 perf): skip the intermediary arrays
9392
let selfUTF8 = Array(String(self).utf8)
9493
selfUTF8.withUnsafeBufferPointer {
@@ -97,54 +96,78 @@ extension _StringGuts {
9796
}
9897
}
9998

100-
internal mutating func append(_ other: _StringGuts) {
101-
defer { self._invariantCheck() }
102-
103-
// Try to form a small string if possible
104-
if !hasNativeStorage {
105-
if let smol = _SmallString(base: self, appending: other) {
106-
self = _StringGuts(smol)
107-
return
108-
}
99+
// Ensure unique native storage with sufficient capacity for the following
100+
// append.
101+
private mutating func prepareForAppendInPlace(
102+
otherUTF8Count otherCount: Int
103+
) {
104+
defer {
105+
_sanityCheck(self.uniqueNativeUnusedCapacity != nil,
106+
"growth should produce uniqueness")
107+
_sanityCheck(self.uniqueNativeUnusedCapacity! >= self.count + otherCount,
108+
"growth should produce enough capacity")
109109
}
110110

111111
// See if we can accomodate without growing or copying. If we have
112112
// sufficient capacity, we do not need to grow, and we can skip the copy if
113113
// unique. Otherwise, growth is required.
114-
let otherUTF8Count = other.utf8Count
115114
let sufficientCapacity: Bool
116-
if let unused = self.nativeUnusedCapacity, unused >= otherUTF8Count {
115+
if let unused = self.nativeUnusedCapacity, unused >= otherCount {
117116
sufficientCapacity = true
118117
} else {
119118
sufficientCapacity = false
120119
}
121-
if !self.isUniqueNative || !sufficientCapacity {
122-
let totalCount = self.utf8Count + otherUTF8Count
123-
124-
// Non-unique storage: just make a copy of the appropriate size, otherwise
125-
// grow like an array.
126-
let growthTarget: Int
127-
if sufficientCapacity {
128-
growthTarget = totalCount
129-
} else {
130-
growthTarget = Swift.max(
131-
totalCount, _growArrayCapacity(nativeCapacity ?? 0))
120+
if self.isUniqueNative && sufficientCapacity {
121+
return
122+
}
123+
124+
let totalCount = self.utf8Count + otherCount
125+
126+
// Non-unique storage: just make a copy of the appropriate size, otherwise
127+
// grow like an array.
128+
let growthTarget: Int
129+
if sufficientCapacity {
130+
growthTarget = totalCount
131+
} else {
132+
growthTarget = Swift.max(
133+
totalCount, _growArrayCapacity(nativeCapacity ?? 0))
134+
}
135+
self.grow(growthTarget)
136+
}
137+
138+
internal mutating func append(_ other: _StringGuts) {
139+
// TODO(UTF8 perf): Minor potential perf win to elevating smol fast-path
140+
// prior to slicing.
141+
append(_SlicedStringGuts(other))
142+
}
143+
144+
internal mutating func append(_ slicedOther: _SlicedStringGuts) {
145+
defer { self._invariantCheck() }
146+
147+
// Try to form a small string if possible
148+
if !hasNativeStorage {
149+
if let smol = _SmallString(
150+
base: _SlicedStringGuts(self), appending: slicedOther
151+
) {
152+
self = _StringGuts(smol)
153+
return
132154
}
133-
self.grow(growthTarget)
134155
}
135156

136-
_sanityCheck(self.uniqueNativeUnusedCapacity != nil,
137-
"growth should produce uniqueness")
157+
prepareForAppendInPlace(otherUTF8Count: slicedOther.utf8Count)
138158

139-
if other.isFastUTF8 {
140-
let otherIsASCII = other.isASCII
141-
other.withFastUTF8 { self.appendInPlace($0, isASCII: otherIsASCII) }
159+
if slicedOther.isFastUTF8 {
160+
let otherIsASCII = slicedOther.isASCII
161+
slicedOther.withFastUTF8 { otherUTF8 in
162+
self.appendInPlace(otherUTF8, isASCII: otherIsASCII)
163+
}
142164
return
143165
}
144-
_foreignAppendInPlace(other)
166+
167+
_foreignAppendInPlace(slicedOther)
145168
}
146169

147-
internal mutating func appendInPlace(
170+
private mutating func appendInPlace(
148171
_ other: UnsafeBufferPointer<UInt8>, isASCII: Bool
149172
) {
150173
self._object.nativeStorage.appendInPlace(other, isASCII: isASCII)
@@ -155,11 +178,11 @@ extension _StringGuts {
155178
}
156179

157180
@inline(never) // slow-path
158-
internal mutating func _foreignAppendInPlace(_ other: _StringGuts) {
181+
private mutating func _foreignAppendInPlace(_ other: _SlicedStringGuts) {
159182
_sanityCheck(!other.isFastUTF8)
160183
_sanityCheck(self.uniqueNativeUnusedCapacity != nil)
161184

162-
var iter = String(other).utf8.makeIterator()
185+
var iter = Substring(other).utf8.makeIterator()
163186
self._object.nativeStorage.appendInPlace(&iter, isASCII: other.isASCII)
164187

165188
// We re-initialize from the modified storage to pick up new count, flags,

stdlib/public/core/StringRangeReplaceableCollection.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,7 @@ extension String: RangeReplaceableCollection {
152152
}
153153

154154
public mutating func append(contentsOf newElements: Substring) {
155-
// TODO(UTF8 perf): This is a horribly slow means...
156-
self.append(newElements._ephemeralString)
155+
self._guts.append(newElements._slicedGuts)
157156
}
158157

159158
/// Appends the characters in the given sequence to the string.

stdlib/public/core/StringStorage.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,19 +157,6 @@ private func determineCodeUnitCapacity(_ desiredCapacity: Int) -> Int {
157157

158158
// Creation
159159
extension _StringStorage {
160-
@_effects(releasenone)
161-
@nonobjc
162-
private static func create(
163-
capacity: Int, countAndFlags: CountAndFlags
164-
) -> _StringStorage {
165-
_sanityCheck(capacity >= countAndFlags.count)
166-
167-
let realCapacity = determineCodeUnitCapacity(capacity)
168-
_sanityCheck(realCapacity > capacity)
169-
return _StringStorage.create(
170-
realCodeUnitCapacity: realCapacity, countAndFlags: countAndFlags)
171-
}
172-
173160
@inline(never) // rdar://problem/44542202
174161
@_effects(releasenone)
175162
@nonobjc
@@ -192,6 +179,19 @@ extension _StringStorage {
192179
return storage
193180
}
194181

182+
@_effects(releasenone)
183+
@nonobjc
184+
private static func create(
185+
capacity: Int, countAndFlags: CountAndFlags
186+
) -> _StringStorage {
187+
_sanityCheck(capacity >= countAndFlags.count)
188+
189+
let realCapacity = determineCodeUnitCapacity(capacity)
190+
_sanityCheck(realCapacity > capacity)
191+
return _StringStorage.create(
192+
realCodeUnitCapacity: realCapacity, countAndFlags: countAndFlags)
193+
}
194+
195195
@_effects(releasenone)
196196
@nonobjc
197197
internal static func create(

stdlib/public/core/Substring.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ public struct Substring {
105105
_invariantCheck()
106106
}
107107

108+
@inline(__always)
109+
internal init(_ slice: _SlicedStringGuts) {
110+
self.init(String(slice._guts)[slice.range])
111+
}
112+
108113
/// Creates an empty substring.
109114
@inlinable @inline(__always)
110115
public init() {

0 commit comments

Comments
 (0)