Skip to content

Commit 3fe6b65

Browse files
authored
Merge pull request #24303 from Catfish-Man/uninitialized-initialize
Add the new uninitialized buffer String initializer privately and use it to fix a perf TODO in append
2 parents 3d5cb90 + 35e21b0 commit 3fe6b65

File tree

1 file changed

+73
-20
lines changed

1 file changed

+73
-20
lines changed

stdlib/public/core/StringGutsRangeReplaceable.swift

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,19 @@ extension _StringGuts {
9797

9898
@inline(never) // slow-path
9999
private mutating func _foreignGrow(_ n: Int) {
100-
// TODO(String performance): Skip intermediary array, transcode directly
101-
// into a StringStorage space.
102-
let selfUTF8 = Array(String(self).utf8)
103-
selfUTF8.withUnsafeBufferPointer {
104-
self = _StringGuts(__StringStorage.create(
105-
initializingFrom: $0, capacity: n, isASCII: self.isASCII))
100+
let newString = String(uninitializedCapacity: n) { buffer in
101+
guard let count = _foreignCopyUTF8(into: buffer) else {
102+
fatalError("String capacity was smaller than required")
103+
}
104+
return count
106105
}
106+
self = newString._guts
107107
}
108108

109109
// Ensure unique native storage with sufficient capacity for the following
110110
// append.
111111
private mutating func prepareForAppendInPlace(
112+
totalCount: Int,
112113
otherUTF8Count otherCount: Int
113114
) {
114115
defer {
@@ -127,11 +128,13 @@ extension _StringGuts {
127128
} else {
128129
sufficientCapacity = false
129130
}
131+
130132
if self.isUniqueNative && sufficientCapacity {
131133
return
132134
}
133-
134-
let totalCount = self.utf8Count + otherCount
135+
136+
// If we have to resize anyway, and we fit in smol, we should have made one
137+
_internalInvariant(totalCount > _SmallString.capacity)
135138

136139
// Non-unique storage: just make a copy of the appropriate size, otherwise
137140
// grow like an array.
@@ -152,25 +155,76 @@ extension _StringGuts {
152155
return
153156
}
154157
}
155-
156158
append(_StringGutsSlice(other))
157159
}
160+
161+
@inline(never)
162+
@_effects(readonly)
163+
private func _foreignConvertedToSmall() -> _SmallString {
164+
let smol = String(uninitializedCapacity: _SmallString.capacity) { buffer in
165+
guard let count = _foreignCopyUTF8(into: buffer) else {
166+
fatalError("String capacity was smaller than required")
167+
}
168+
return count
169+
}
170+
_internalInvariant(smol._guts.isSmall)
171+
return smol._guts.asSmall
172+
}
173+
174+
private func _convertedToSmall() -> _SmallString {
175+
_internalInvariant(utf8Count <= _SmallString.capacity)
176+
if _fastPath(isSmall) {
177+
return asSmall
178+
}
179+
if isFastUTF8 {
180+
return withFastUTF8 { _SmallString($0)! }
181+
}
182+
return _foreignConvertedToSmall()
183+
}
158184

159185
internal mutating func append(_ slicedOther: _StringGutsSlice) {
160186
defer { self._invariantCheck() }
161187

162-
if self.isSmall && slicedOther._guts.isSmall {
188+
let otherCount = slicedOther.utf8Count
189+
190+
let totalCount = utf8Count + otherCount
191+
192+
/*
193+
Goal: determine if we need to allocate new native capacity
194+
Possible scenarios in which we need to allocate:
195+
• Not uniquely owned and native: we can't use the capacity to grow into,
196+
have to become unique + native by allocating
197+
• Not enough capacity: have to allocate to grow
198+
199+
Special case: a non-smol String that can fit in a smol String but doesn't
200+
meet the above criteria shouldn't throw away its buffer just to be smol.
201+
The reasoning here is that it may be bridged or have reserveCapacity'd
202+
in preparation for appending more later, in which case we would end up
203+
have to allocate anyway to convert back from smol.
204+
205+
If we would have to re-allocate anyway then that's not a problem and we
206+
should just be smol.
207+
208+
e.g. consider
209+
var str = "" // smol
210+
str.reserveCapacity(100) // large native unique
211+
str += "<html>" // don't convert back to smol here!
212+
str += htmlContents // because this would have to anyway!
213+
*/
214+
let hasEnoughUsableSpace = isUniqueNative &&
215+
nativeUnusedCapacity! >= otherCount
216+
let shouldBeSmol = totalCount <= _SmallString.capacity &&
217+
(isSmall || !hasEnoughUsableSpace)
218+
219+
if shouldBeSmol {
220+
let smolSelf = _convertedToSmall()
221+
let smolOther = String(Substring(slicedOther))._guts._convertedToSmall()
163222
// TODO: In-register slicing
164-
let smolSelf = self.asSmall
165-
if let smol = slicedOther.withFastUTF8({ otherUTF8 in
166-
return _SmallString(smolSelf, appending: _SmallString(otherUTF8)!)
167-
}) {
168-
self = _StringGuts(smol)
169-
return
170-
}
223+
self = _StringGuts(_SmallString(smolSelf, appending: smolOther)!)
224+
return
171225
}
172-
173-
prepareForAppendInPlace(otherUTF8Count: slicedOther.utf8Count)
226+
227+
prepareForAppendInPlace(totalCount: totalCount, otherUTF8Count: otherCount)
174228

175229
if slicedOther.isFastUTF8 {
176230
let otherIsASCII = slicedOther.isASCII
@@ -242,7 +296,6 @@ extension _StringGuts {
242296
self = result._guts
243297
}
244298

245-
@inline(__always) // Always-specialize
246299
internal mutating func replaceSubrange<C>(
247300
_ bounds: Range<Index>,
248301
with newElements: C

0 commit comments

Comments
 (0)