Skip to content

Commit 8082b58

Browse files
authored
Merge pull request #34961 from lorentey/buffers-need-to-be-fast-but-not-too-fast
[stdlib] Review and fix some problems with unsafe buffer and Range initialization
2 parents 9e198ab + 94a7eee commit 8082b58

13 files changed

+130
-38
lines changed

stdlib/public/core/ClosedRange.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ public struct ClosedRange<Bound: Comparable> {
6868
/// The range's upper bound.
6969
public let upperBound: Bound
7070

71+
// This works around _debugPrecondition() impacting the performance of
72+
// optimized code. (rdar://72246338)
73+
@_alwaysEmitIntoClient @inline(__always)
74+
internal init(_uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
75+
self.lowerBound = bounds.lower
76+
self.upperBound = bounds.upper
77+
}
78+
7179
/// Creates an instance with the given bounds.
7280
///
7381
/// Because this initializer does not perform any checks, it should be used
@@ -78,8 +86,9 @@ public struct ClosedRange<Bound: Comparable> {
7886
/// - Parameter bounds: A tuple of the lower and upper bounds of the range.
7987
@inlinable
8088
public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
81-
self.lowerBound = bounds.lower
82-
self.upperBound = bounds.upper
89+
_debugPrecondition(bounds.lower <= bounds.upper,
90+
"ClosedRange requires lowerBound <= upperBound")
91+
self.init(_uncheckedBounds: (lower: bounds.lower, upper: bounds.upper))
8392
}
8493
}
8594

@@ -100,8 +109,9 @@ extension ClosedRange: RangeExpression {
100109
public func relative<C: Collection>(to collection: C) -> Range<Bound>
101110
where C.Index == Bound {
102111
return Range(
103-
uncheckedBounds: (
104-
lower: lowerBound, upper: collection.index(after: self.upperBound)))
112+
_uncheckedBounds: (
113+
lower: lowerBound,
114+
upper: collection.index(after: self.upperBound)))
105115
}
106116

107117
/// Returns a Boolean value indicating whether the given element is contained
@@ -336,7 +346,7 @@ extension Comparable {
336346
public static func ... (minimum: Self, maximum: Self) -> ClosedRange<Self> {
337347
_precondition(
338348
minimum <= maximum, "Range requires lowerBound <= upperBound")
339-
return ClosedRange(uncheckedBounds: (lower: minimum, upper: maximum))
349+
return ClosedRange(_uncheckedBounds: (lower: minimum, upper: maximum))
340350
}
341351
}
342352

@@ -423,7 +433,7 @@ extension ClosedRange {
423433
limits.upperBound < self.upperBound ? limits.upperBound
424434
: limits.lowerBound > self.upperBound ? limits.lowerBound
425435
: self.upperBound
426-
return ClosedRange(uncheckedBounds: (lower: lower, upper: upper))
436+
return ClosedRange(_uncheckedBounds: (lower: lower, upper: upper))
427437
}
428438
}
429439

@@ -439,7 +449,7 @@ extension ClosedRange where Bound: Strideable, Bound.Stride: SignedInteger {
439449
public init(_ other: Range<Bound>) {
440450
_precondition(!other.isEmpty, "Can't form an empty closed range")
441451
let upperBound = other.upperBound.advanced(by: -1)
442-
self.init(uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
452+
self.init(_uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
443453
}
444454
}
445455

@@ -476,7 +486,7 @@ extension ClosedRange: Decodable where Bound: Decodable {
476486
codingPath: decoder.codingPath,
477487
debugDescription: "Cannot initialize \(ClosedRange.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
478488
}
479-
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
489+
self.init(_uncheckedBounds: (lower: lowerBound, upper: upperBound))
480490
}
481491
}
482492

stdlib/public/core/Range.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ public struct Range<Bound: Comparable> {
157157
/// instance does not contain its upper bound.
158158
public let upperBound: Bound
159159

160+
// This works around _debugPrecondition() impacting the performance of
161+
// optimized code. (rdar://72246338)
162+
@_alwaysEmitIntoClient @inline(__always)
163+
internal init(_uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
164+
self.lowerBound = bounds.lower
165+
self.upperBound = bounds.upper
166+
}
167+
160168
/// Creates an instance with the given bounds.
161169
///
162170
/// Because this initializer does not perform any checks, it should be used
@@ -167,8 +175,9 @@ public struct Range<Bound: Comparable> {
167175
/// - Parameter bounds: A tuple of the lower and upper bounds of the range.
168176
@inlinable
169177
public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
170-
self.lowerBound = bounds.lower
171-
self.upperBound = bounds.upper
178+
_debugPrecondition(bounds.lower <= bounds.upper,
179+
"Range requires lowerBound <= upperBound")
180+
self.init(_uncheckedBounds: (lower: bounds.lower, upper: bounds.upper))
172181
}
173182

174183
/// Returns a Boolean value indicating whether the given element is contained
@@ -308,7 +317,7 @@ extension Range where Bound: Strideable, Bound.Stride: SignedInteger {
308317
/// require an upper bound of `Int.max + 1`, which is not representable as
309318
public init(_ other: ClosedRange<Bound>) {
310319
let upperBound = other.upperBound.advanced(by: 1)
311-
self.init(uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
320+
self.init(_uncheckedBounds: (lower: other.lowerBound, upper: upperBound))
312321
}
313322
}
314323

@@ -325,7 +334,7 @@ extension Range: RangeExpression {
325334
@inlinable // trivial-implementation
326335
public func relative<C: Collection>(to collection: C) -> Range<Bound>
327336
where C.Index == Bound {
328-
return Range(uncheckedBounds: (lower: lowerBound, upper: upperBound))
337+
self
329338
}
330339
}
331340

@@ -359,7 +368,7 @@ extension Range {
359368
limits.upperBound < self.upperBound ? limits.upperBound
360369
: limits.lowerBound > self.upperBound ? limits.lowerBound
361370
: self.upperBound
362-
return Range(uncheckedBounds: (lower: lower, upper: upper))
371+
return Range(_uncheckedBounds: (lower: lower, upper: upper))
363372
}
364373
}
365374

@@ -435,7 +444,7 @@ extension Range: Decodable where Bound: Decodable {
435444
codingPath: decoder.codingPath,
436445
debugDescription: "Cannot initialize \(Range.self) with a lowerBound (\(lowerBound)) greater than upperBound (\(upperBound))"))
437446
}
438-
self.init(uncheckedBounds: (lower: lowerBound, upper: upperBound))
447+
self.init(_uncheckedBounds: (lower: lowerBound, upper: upperBound))
439448
}
440449
}
441450

@@ -729,7 +738,7 @@ extension Comparable {
729738
public static func ..< (minimum: Self, maximum: Self) -> Range<Self> {
730739
_precondition(minimum <= maximum,
731740
"Range requires lowerBound <= upperBound")
732-
return Range(uncheckedBounds: (lower: minimum, upper: maximum))
741+
return Range(_uncheckedBounds: (lower: minimum, upper: maximum))
733742
}
734743

735744
/// Returns a partial range up to, but not including, its upper bound.

stdlib/public/core/SmallString.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ extension _SmallString {
208208
// Restore the memory type of self._storage
209209
_ = rawPtr.bindMemory(to: RawBitPattern.self, capacity: 1)
210210
}
211-
return try f(UnsafeBufferPointer(start: ptr, count: count))
211+
return try f(UnsafeBufferPointer(_uncheckedStart: ptr, count: count))
212212
}
213213
}
214214

stdlib/public/core/String.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ extension String {
458458
contigBytes._providesContiguousBytesNoCopy
459459
{
460460
self = contigBytes.withUnsafeBytes { rawBufPtr in
461+
Builtin.onFastPath() // encourage SIL Optimizer to inline this closure
461462
return String._fromUTF8Repairing(
462463
UnsafeBufferPointer(
463464
start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),

stdlib/public/core/StringObject.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@ extension _StringObject {
902902
return sharedUTF8
903903
}
904904
return UnsafeBufferPointer(
905-
start: self.nativeUTF8Start, count: self.largeCount)
905+
_uncheckedStart: self.nativeUTF8Start, count: self.largeCount)
906906
}
907907

908908
// Whether the object stored can be bridged directly as a NSString

stdlib/public/core/StringProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ extension StringProtocol {
153153
let end = endIndex
154154
_internalInvariant(
155155
start.transcodedOffset == 0 && end.transcodedOffset == 0)
156-
return Range(uncheckedBounds: (start._encodedOffset, end._encodedOffset))
156+
return Range(_uncheckedBounds: (start._encodedOffset, end._encodedOffset))
157157
}
158158
}
159159

stdlib/public/core/StringStorageBridge.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extension _AbstractStringStorage {
3030
"Range out of bounds")
3131

3232
let range = Range(
33-
uncheckedBounds: (aRange.location, aRange.location+aRange.length))
33+
_uncheckedBounds: (aRange.location, aRange.location+aRange.length))
3434
let str = asString
3535
str._copyUTF16CodeUnits(
3636
into: UnsafeMutableBufferPointer(start: buffer, count: range.count),

stdlib/public/core/Substring.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public struct Substring {
105105

106106
self._slice = Slice(
107107
base: slice.base,
108-
bounds: Range(uncheckedBounds: (start, end)))
108+
bounds: Range(_uncheckedBounds: (start, end)))
109109
_invariantCheck()
110110
}
111111

@@ -132,7 +132,7 @@ extension Substring {
132132
@inlinable @inline(__always)
133133
internal var _offsetRange: Range<Int> {
134134
return Range(
135-
uncheckedBounds: (startIndex._encodedOffset, endIndex._encodedOffset))
135+
_uncheckedBounds: (startIndex._encodedOffset, endIndex._encodedOffset))
136136
}
137137

138138
#if !INTERNAL_CHECKS_ENABLED
@@ -322,7 +322,8 @@ extension Substring: CustomDebugStringConvertible {
322322

323323
extension Substring: LosslessStringConvertible {
324324
public init(_ content: String) {
325-
self = content[...]
325+
let range = Range(_uncheckedBounds: (content.startIndex, content.endIndex))
326+
self.init(Slice(base: content, bounds: range))
326327
}
327328
}
328329

@@ -727,14 +728,14 @@ extension Substring: RangeReplaceableCollection {
727728
public init<S: Sequence>(_ elements: S)
728729
where S.Element == Character {
729730
if let str = elements as? String {
730-
self = str[...]
731+
self.init(str)
731732
return
732733
}
733734
if let subStr = elements as? Substring {
734735
self = subStr
735736
return
736737
}
737-
self = String(elements)[...]
738+
self.init(String(elements))
738739
}
739740

740741
@inlinable // specialize

stdlib/public/core/UnsafeBufferPointer.swift.gyb

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,14 @@ extension Unsafe${Mutable}BufferPointer: ${Mutable}Collection, RandomAccessColle
207207
// optimizer is not capable of creating partial specializations yet.
208208
// NOTE: Range checks are not performed here, because it is done later by
209209
// the subscript function.
210-
return end - start
210+
// NOTE: We allow the subtraction to silently overflow in release builds
211+
// to eliminate a superflous check when `start` and `end` are both valid
212+
// indices. (The operation can only overflow if `start` is negative, which
213+
// implies it's an invalid index.) `Collection` does not specify what
214+
// `distance` should return when given an invalid index pair.
215+
let result = end.subtractingReportingOverflow(start)
216+
_debugPrecondition(!result.overflow)
217+
return result.partialValue
211218
}
212219

213220
@inlinable // unsafe-performance
@@ -387,6 +394,17 @@ extension Unsafe${Mutable}BufferPointer: ${Mutable}Collection, RandomAccessColle
387394
}
388395

389396
extension Unsafe${Mutable}BufferPointer {
397+
// This works around _debugPrecondition() impacting the performance of
398+
// optimized code. (rdar://72246338)
399+
@_alwaysEmitIntoClient
400+
internal init(
401+
@_nonEphemeral _uncheckedStart start: Unsafe${Mutable}Pointer<Element>?,
402+
count: Int
403+
) {
404+
_position = start
405+
self.count = count
406+
}
407+
390408
/// Creates a new buffer pointer over the specified number of contiguous
391409
/// instances beginning at the given pointer.
392410
///
@@ -401,13 +419,12 @@ extension Unsafe${Mutable}BufferPointer {
401419
public init(
402420
@_nonEphemeral start: Unsafe${Mutable}Pointer<Element>?, count: Int
403421
) {
404-
_precondition(
422+
_debugPrecondition(
405423
count >= 0, "Unsafe${Mutable}BufferPointer with negative count")
406-
_precondition(
424+
_debugPrecondition(
407425
count == 0 || start != nil,
408426
"Unsafe${Mutable}BufferPointer has a nil start and nonzero count")
409-
_position = start
410-
self.count = count
427+
self.init(_uncheckedStart: start, count: _assumeNonNegative(count))
411428
}
412429

413430
@inlinable // unsafe-performance
@@ -498,6 +515,17 @@ extension Unsafe${Mutable}BufferPointer {
498515
/// - Parameter slice: The buffer slice to rebase.
499516
@inlinable // unsafe-performance
500517
public init(rebasing slice: Slice<UnsafeBufferPointer<Element>>) {
518+
// NOTE: `Slice` does not guarantee that its start/end indices are valid
519+
// in `base` -- it merely ensures that `startIndex <= endIndex`.
520+
// We need manually check that we aren't given an invalid slice,
521+
// or the resulting collection would allow access that was
522+
// out-of-bounds with respect to the original base buffer.
523+
// We only do this in debug builds to prevent a measurable performance
524+
// degradation wrt passing around pointers not wrapped in a BufferPointer
525+
// construct.
526+
_debugPrecondition(
527+
slice.startIndex >= 0 && slice.endIndex <= slice.base.count,
528+
"Invalid slice")
501529
let base = slice.base.baseAddress?.advanced(by: slice.startIndex)
502530
let count = slice.endIndex &- slice.startIndex
503531
self.init(start: base, count: count)

stdlib/public/core/UnsafeRawBufferPointer.swift.gyb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ extension Unsafe${Mutable}RawBufferPointer: ${Mutable}Collection {
253253
@inlinable
254254
public var count: Int {
255255
if let pos = _position {
256-
return _end! - pos
256+
return _assumeNonNegative(_end! - pos)
257257
}
258258
return 0
259259
}
@@ -432,11 +432,11 @@ extension Unsafe${Mutable}RawBufferPointer {
432432
public init(
433433
@_nonEphemeral start: Unsafe${Mutable}RawPointer?, count: Int
434434
) {
435-
_precondition(count >= 0, "${Self} with negative count")
436-
_precondition(count == 0 || start != nil,
435+
_debugPrecondition(count >= 0, "${Self} with negative count")
436+
_debugPrecondition(count == 0 || start != nil,
437437
"${Self} has a nil start and nonzero count")
438438
_position = start
439-
_end = start.map { $0 + count }
439+
_end = start.map { $0 + _assumeNonNegative(count) }
440440
}
441441

442442
/// Creates a new buffer over the same memory as the given buffer.
@@ -511,6 +511,17 @@ extension Unsafe${Mutable}RawBufferPointer {
511511
/// - Parameter slice: The raw buffer slice to rebase.
512512
@inlinable
513513
public init(rebasing slice: Slice<UnsafeRawBufferPointer>) {
514+
// NOTE: `Slice` does not guarantee that its start/end indices are valid
515+
// in `base` -- it merely ensures that `startIndex <= endIndex`.
516+
// We need manually check that we aren't given an invalid slice,
517+
// or the resulting collection would allow access that was
518+
// out-of-bounds with respect to the original base buffer.
519+
// We only do this in debug builds to prevent a measurable performance
520+
// degradation wrt passing around pointers not wrapped in a BufferPointer
521+
// construct.
522+
_debugPrecondition(
523+
slice.startIndex >= 0 && slice.endIndex <= slice.base.count,
524+
"Invalid slice")
514525
let base = slice.base.baseAddress?.advanced(by: slice.startIndex)
515526
let count = slice.endIndex &- slice.startIndex
516527
self.init(start: base, count: count)

test/stdlib/RangeTraps.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,23 @@ RangeTraps.test("throughNaN")
116116
_ = ...Double.nan
117117
}
118118

119+
RangeTraps.test("UncheckedHalfOpen")
120+
.xfail(.custom(
121+
{ !_isDebugAssertConfiguration() },
122+
reason: "assertions are disabled in Release and Unchecked mode"))
123+
.code {
124+
expectCrashLater()
125+
var range = Range(uncheckedBounds: (lower: 1, upper: 0))
126+
}
127+
128+
RangeTraps.test("UncheckedClosed")
129+
.xfail(.custom(
130+
{ !_isDebugAssertConfiguration() },
131+
reason: "assertions are disabled in Release and Unchecked mode"))
132+
.code {
133+
expectCrashLater()
134+
var range = ClosedRange(uncheckedBounds: (lower: 1, upper: 0))
135+
}
136+
119137
runAllTests()
120138

validation-test/stdlib/ArrayTraps.swift.gyb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ ${ArrayTy}Traps.test("${'remove(at:)/%s' % index}")
172172

173173
ArrayTraps.test("unsafeLength")
174174
.skip(.custom(
175-
{ _isFastAssertConfiguration() },
176-
reason: "this trap is not guaranteed to happen in -Ounchecked"))
175+
{ !_isDebugAssertConfiguration() },
176+
reason: "this trap is not guaranteed to happen in -O or -Ounchecked"))
177177
.crashOutputMatches(_isDebugAssertConfiguration() ?
178178
"UnsafeBufferPointer with negative count" : "")
179179
.code {

0 commit comments

Comments
 (0)