Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bf9d50c

Browse files
committedJan 31, 2024
Add "unchecked" subscripts for buffers and arrays
Introduce `subscript(unchecked:)` and `subscript(uncheckedBounds:)` operations for the various buffer and array types, allowing one to explicitly opt out of bounds checking for release builds.
1 parent 5c0b803 commit bf9d50c

8 files changed

+317
-2
lines changed
 

‎stdlib/public/Differentiation/ArrayDifferentiation.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ extension Array: Differentiable where Element: Differentiable {
193193

194194
extension Array where Element: Differentiable {
195195
@usableFromInline
196-
@derivative(of: subscript)
196+
@derivative(of: subscript(_:))
197197
func _vjpSubscript(index: Int) -> (
198198
value: Element, pullback: (Element.TangentVector) -> TangentVector
199199
) {
@@ -208,7 +208,7 @@ extension Array where Element: Differentiable {
208208
}
209209

210210
@usableFromInline
211-
@derivative(of: subscript)
211+
@derivative(of: subscript(_:))
212212
func _jvpSubscript(index: Int) -> (
213213
value: Element, differential: (TangentVector) -> Element.TangentVector
214214
) {

‎stdlib/public/core/Array.swift

+100
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,28 @@ extension Array {
400400
return _DependenceToken()
401401
}
402402

403+
/// Check that the given `index` is valid for subscripting, i.e.
404+
/// `0 ≤ index < count`, but only in debug buikds.
405+
@inlinable
406+
@_semantics("array.check_subscript")
407+
@_effects(notEscaping self.**)
408+
public // @testable
409+
func _debugCheckSubscript(
410+
_ index: Int, wasNativeTypeChecked: Bool
411+
) -> _DependenceToken {
412+
#if _runtime(_ObjC)
413+
// There is no need to do bounds checking for the non-native case because
414+
// ObjectiveC arrays do bounds checking by their own.
415+
// And in the native-non-type-checked case, it's also not needed to do bounds
416+
// checking here, because it's done in ArrayBuffer._getElementSlowPath.
417+
if _fastPath(wasNativeTypeChecked) {
418+
_buffer._native._debugCheckValidSubscript(index)
419+
}
420+
#else
421+
_buffer._debugCheckValidSubscript(index)
422+
#endif
423+
return _DependenceToken()
424+
}
403425
/// Check that the given `index` is valid for subscripting, i.e.
404426
/// `0 ≤ index < count`.
405427
///
@@ -411,6 +433,17 @@ extension Array {
411433
_buffer._checkValidSubscriptMutating(index)
412434
}
413435

436+
/// Check that the given `index` is valid for subscripting, i.e.
437+
/// `0 ≤ index < count`, but only in debug configurations.
438+
///
439+
/// - Precondition: The buffer must be uniquely referenced and native.
440+
@_alwaysEmitIntoClient
441+
@_semantics("array.check_subscript")
442+
@_effects(notEscaping self.**)
443+
internal func _debugCheckSubscript_mutating(_ index: Int) {
444+
_buffer._debugCheckValidSubscriptMutating(index)
445+
}
446+
414447
/// Check that the specified `index` is valid, i.e. `0 ≤ index ≤ count`.
415448
@inlinable
416449
@_semantics("array.check_index")
@@ -420,6 +453,16 @@ extension Array {
420453
_precondition(index >= startIndex, "Negative Array index is out of range")
421454
}
422455

456+
/// Check that the specified `index` is valid, i.e. `0 ≤ index ≤ count`,
457+
/// but only in debug configurations.
458+
@inlinable
459+
@_semantics("array.check_index")
460+
@_effects(notEscaping self.**)
461+
internal func _debugCheckIndex(_ index: Int) {
462+
_debugPrecondition(index <= endIndex, "Array index is out of range")
463+
_debugPrecondition(index >= startIndex, "Negative Array index is out of range")
464+
}
465+
423466
@_semantics("array.get_element")
424467
@_effects(notEscaping self.value**)
425468
@_effects(escaping self.value**.class*.value** -> return.value**)
@@ -761,6 +804,38 @@ extension Array: RandomAccessCollection, MutableCollection {
761804
}
762805
}
763806

807+
/// Accesses the element at the specified position without bounds
808+
/// checking.
809+
///
810+
/// This unsafe operation should only be used when performance analysis
811+
/// has determined that the bounds checks are not being eliminated
812+
/// by the optimizer despite being ensured by a higher-level invariant.
813+
@inlinable @_alwaysEmitIntoClient
814+
public subscript(unchecked index: Int) -> Element {
815+
get {
816+
// This call may be hoisted or eliminated by the optimizer. If
817+
// there is an inout violation, this value may be stale so needs to be
818+
// checked again below.
819+
let wasNativeTypeChecked = _hoistableIsNativeTypeChecked()
820+
821+
// Make sure the index is in range and wasNativeTypeChecked is
822+
// still valid.
823+
let token = _debugCheckSubscript(
824+
index, wasNativeTypeChecked: wasNativeTypeChecked)
825+
826+
return _getElement(
827+
index, wasNativeTypeChecked: wasNativeTypeChecked,
828+
matchingSubscriptCheck: token)
829+
}
830+
_modify {
831+
_makeMutableAndUnique() // makes the array native, too
832+
_debugCheckSubscript_mutating(index)
833+
let address = _buffer.mutableFirstElementAddress + index
834+
defer { _endMutation() }
835+
yield &address.pointee
836+
}
837+
}
838+
764839
/// Accesses a contiguous subrange of the array's elements.
765840
///
766841
/// The returned `ArraySlice` instance uses the same indices for the same
@@ -804,6 +879,31 @@ extension Array: RandomAccessCollection, MutableCollection {
804879
}
805880
}
806881

882+
/// Accesses a contiguous subrange of the array's elements without
883+
/// bounds checks on the range.
884+
///
885+
/// This unsafe operation should only be used when performance analysis
886+
/// has determined that the bounds checks are not being eliminated
887+
/// by the optimizer despite being ensured by a higher-level invariant.
888+
@inlinable @_alwaysEmitIntoClient
889+
public subscript(uncheckedBounds bounds: Range<Int>) -> ArraySlice<Element> {
890+
get {
891+
_debugCheckIndex(bounds.lowerBound)
892+
_debugCheckIndex(bounds.upperBound)
893+
return ArraySlice(_buffer: _buffer[bounds])
894+
}
895+
set(rhs) {
896+
_debugCheckIndex(bounds.lowerBound)
897+
_debugCheckIndex(bounds.upperBound)
898+
// If the replacement buffer has same identity, and the ranges match,
899+
// then this was a pinned in-place modification, nothing further needed.
900+
if self[bounds]._buffer.identity != rhs._buffer.identity
901+
|| bounds != rhs.startIndex..<rhs.endIndex {
902+
self.replaceSubrange(bounds, with: rhs)
903+
}
904+
}
905+
}
906+
807907
/// The number of elements in the array.
808908
@inlinable
809909
@_semantics("array.get_count")

‎stdlib/public/core/ArrayBuffer.swift

+8
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,14 @@ extension _ArrayBuffer {
467467
_native._checkValidSubscriptMutating(index)
468468
}
469469

470+
/// Traps unless the given `index` is valid for subscripting, i.e.
471+
/// `0 ≤ index < count`.
472+
///
473+
/// - Precondition: The buffer must be mutable.
474+
@_alwaysEmitIntoClient
475+
internal func _debugCheckValidSubscriptMutating(_ index: Int) {
476+
_native._debugCheckValidSubscriptMutating(index)
477+
}
470478
/// The number of elements the buffer can store without reallocation.
471479
///
472480
/// This property is obsolete. It's only used for the ArrayBufferProtocol and

‎stdlib/public/core/ContiguousArray.swift

+71
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ extension ContiguousArray {
9191
_buffer._checkValidSubscript(index)
9292
}
9393

94+
/// Check that the given `index` is valid for subscripting, i.e.
95+
/// `0 ≤ index < count`, but only in debug builds.
96+
@inlinable
97+
@inline(__always)
98+
internal func _debugCheckSubscript_native(_ index: Int) {
99+
_buffer._debugCheckValidSubscript(index)
100+
}
101+
94102
/// Check that the given `index` is valid for subscripting, i.e.
95103
/// `0 ≤ index < count`.
96104
///
@@ -101,6 +109,16 @@ extension ContiguousArray {
101109
_buffer._checkValidSubscriptMutating(index)
102110
}
103111

112+
/// Check that the given `index` is valid for subscripting, i.e.
113+
/// `0 ≤ index < count`, but only in debug builds.
114+
///
115+
/// - Precondition: The buffer must be uniquely referenced and native.
116+
@_alwaysEmitIntoClient
117+
@_semantics("array.check_subscript")
118+
internal func _debugCheckSubscript_mutating(_ index: Int) {
119+
_buffer._debugCheckValidSubscriptMutating(index)
120+
}
121+
104122
/// Check that the specified `index` is valid, i.e. `0 ≤ index ≤ count`.
105123
@inlinable
106124
@_semantics("array.check_index")
@@ -109,6 +127,14 @@ extension ContiguousArray {
109127
_precondition(index >= startIndex, "Negative ContiguousArray index is out of range")
110128
}
111129

130+
/// Check that the specified `index` is valid, i.e. `0 ≤ index ≤ count`, but only in debug builds.
131+
@inlinable
132+
@_semantics("array.check_index")
133+
internal func _debugCheckIndex(_ index: Int) {
134+
_debugPrecondition(index <= endIndex, "ContiguousArray index is out of range")
135+
_debugPrecondition(index >= startIndex, "Negative ContiguousArray index is out of range")
136+
}
137+
112138
@inlinable
113139
@_semantics("array.get_element_address")
114140
internal func _getElementAddress(_ index: Int) -> UnsafeMutablePointer<Element> {
@@ -421,6 +447,27 @@ extension ContiguousArray: RandomAccessCollection, MutableCollection {
421447
}
422448
}
423449

450+
/// Accesses the element at the specified position without
451+
/// bounds checking.
452+
///
453+
/// This unsafe operation should only be used when performance analysis
454+
/// has determined that the bounds checks are not being eliminated
455+
/// by the optimizer despite being ensured by a higher-level invariant.
456+
@inlinable @_alwaysEmitIntoClient
457+
public subscript(unchecked index: Int) -> Element {
458+
get {
459+
_debugCheckSubscript_native(index)
460+
return _buffer.getElement(index)
461+
}
462+
_modify {
463+
_makeMutableAndUnique()
464+
_debugCheckSubscript_mutating(index)
465+
let address = _buffer.mutableFirstElementAddress + index
466+
defer { _endMutation() }
467+
yield &address.pointee
468+
}
469+
}
470+
424471
/// Accesses a contiguous subrange of the array's elements.
425472
///
426473
/// The returned `ArraySlice` instance uses the same indices for the same
@@ -464,6 +511,30 @@ extension ContiguousArray: RandomAccessCollection, MutableCollection {
464511
}
465512
}
466513

514+
/// Accesses a contiguous subrange of the array's elements.
515+
///
516+
/// This unsafe operation should only be used when performance analysis
517+
/// has determined that the bounds checks are not being eliminated
518+
/// by the optimizer despite being ensured by a higher-level invariant.
519+
@inlinable @_alwaysEmitIntoClient
520+
public subscript(uncheckedBounds bounds: Range<Int>) -> ArraySlice<Element> {
521+
get {
522+
_debugCheckIndex(bounds.lowerBound)
523+
_debugCheckIndex(bounds.upperBound)
524+
return ArraySlice(_buffer: _buffer[bounds])
525+
}
526+
set(rhs) {
527+
_debugCheckIndex(bounds.lowerBound)
528+
_debugCheckIndex(bounds.upperBound)
529+
// If the replacement buffer has same identity, and the ranges match,
530+
// then this was a pinned in-place modification, nothing further needed.
531+
if self[bounds]._buffer.identity != rhs._buffer.identity
532+
|| bounds != rhs.startIndex..<rhs.endIndex {
533+
self.replaceSubrange(bounds, with: rhs)
534+
}
535+
}
536+
}
537+
467538
/// The number of elements in the array.
468539
@inlinable
469540
public var count: Int {

‎stdlib/public/core/ContiguousArrayBuffer.swift

+26
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,19 @@ internal struct _ContiguousArrayBuffer<Element>: _ArrayBufferProtocol {
669669
)
670670
}
671671

672+
/// Traps unless the given `index` is valid for subscripting, i.e.
673+
/// `0 ≤ index < count`, but only in debug mode.
674+
///
675+
/// - Precondition: The buffer must be immutable.
676+
@inlinable
677+
@inline(__always)
678+
internal func _debugCheckValidSubscript(_ index: Int) {
679+
_debugPrecondition(
680+
(index >= 0) && (index < immutableCount),
681+
"Index out of range"
682+
)
683+
}
684+
672685
/// Traps unless the given `index` is valid for subscripting, i.e.
673686
/// `0 ≤ index < count`.
674687
///
@@ -682,6 +695,19 @@ internal struct _ContiguousArrayBuffer<Element>: _ArrayBufferProtocol {
682695
)
683696
}
684697

698+
/// Traps unless the given `index` is valid for subscripting, i.e.
699+
/// `0 ≤ index < count`, but only in debug builds.
700+
///
701+
/// - Precondition: The buffer must be mutable.
702+
@_alwaysEmitIntoClient
703+
@inline(__always)
704+
internal func _debugCheckValidSubscriptMutating(_ index: Int) {
705+
_debugPrecondition(
706+
(index >= 0) && (index < mutableCount),
707+
"Index out of range"
708+
)
709+
}
710+
685711
/// The number of elements the buffer can store without reallocation.
686712
///
687713
/// This property is obsolete. It's only used for the ArrayBufferProtocol and

‎stdlib/public/core/SliceBuffer.swift

+8
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,14 @@ internal struct _SliceBuffer<Element>
307307
index >= startIndex && index < endIndex, "Index out of bounds")
308308
}
309309

310+
/// Traps unless the given `index` is valid for subscripting, i.e.
311+
/// `startIndex ≤ index < endIndex`, but only in debug builkds.
312+
@inlinable
313+
internal func _debugCheckValidSubscript(_ index: Int) {
314+
_debugPrecondition(
315+
index >= startIndex && index < endIndex, "Index out of bounds")
316+
}
317+
310318
@inlinable
311319
internal var capacity: Int {
312320
let count = self.count

‎stdlib/public/core/UnsafeBufferPointer.swift.gyb

+54
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,28 @@ extension Unsafe${Mutable}BufferPointer: ${Mutable}Collection, RandomAccessColle
353353
%end
354354
}
355355

356+
/// Accesses the element at the specified position without bounds
357+
/// checking.
358+
///
359+
/// This unsafe operation should only be used when performance analysis
360+
/// has determined that the bounds checks are not being eliminated
361+
/// by the optimizer despite being ensured by a higher-level invariant.
362+
@inlinable @_alwaysEmitIntoClient
363+
public subscript(unchecked i: Int) -> Element {
364+
get {
365+
_debugPrecondition(i >= 0)
366+
_debugPrecondition(i < endIndex)
367+
return _position._unsafelyUnwrappedUnchecked[i]
368+
}
369+
%if Mutable:
370+
nonmutating _modify {
371+
_debugPrecondition(i >= 0)
372+
_debugPrecondition(i < endIndex)
373+
yield &_position._unsafelyUnwrappedUnchecked[i]
374+
}
375+
%end
376+
}
377+
356378
// Skip all debug and runtime checks
357379

358380
@inlinable // unsafe-performance
@@ -433,6 +455,38 @@ extension Unsafe${Mutable}BufferPointer: ${Mutable}Collection, RandomAccessColle
433455
}
434456
% end
435457
}
458+
459+
/// Accesses a contiguous subrange of the buffer's elements without
460+
/// bounds checking.
461+
///
462+
/// This unsafe operation should only be used when performance analysis
463+
/// has determined that the bounds checks are not being eliminated
464+
/// by the optimizer despite being ensured by a higher-level invariant.
465+
@inlinable @_alwaysEmitIntoClient
466+
public subscript(uncheckedBounds bounds: Range<Int>)
467+
-> Slice<Unsafe${Mutable}BufferPointer<Element>>
468+
{
469+
get {
470+
_debugPrecondition(bounds.lowerBound >= startIndex)
471+
_debugPrecondition(bounds.upperBound <= endIndex)
472+
return Slice(
473+
base: self, bounds: bounds)
474+
}
475+
% if Mutable:
476+
nonmutating set {
477+
_debugPrecondition(bounds.lowerBound >= startIndex)
478+
_debugPrecondition(bounds.upperBound <= endIndex)
479+
_debugPrecondition(bounds.count == newValue.count)
480+
481+
if !newValue.isEmpty {
482+
(_position! + bounds.lowerBound).update(
483+
from: newValue.base._position! + newValue.startIndex,
484+
count: newValue.count)
485+
}
486+
}
487+
% end
488+
}
489+
436490
% if mutable:
437491

438492
/// Exchanges the values at the specified indices of the buffer.

‎test/stdlib/BoundsCheckTraps.swift

+48
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,52 @@ BoundsCheckTraps.test("Character")
8888
_blackHole(char)
8989
}
9090

91+
BoundsCheckTraps.test("ArrayUnchecked")
92+
.skip(.custom(
93+
{ _isFastAssertConfiguration() || _isReleaseAssertConfiguration() || _isReleaseAssertWithBoundsSafetyConfiguration() },
94+
reason: "this trap is not guaranteed to happen"))
95+
.code {
96+
expectCrashLater()
97+
var array = [1, 2]
98+
array.append(3)
99+
let value = array[unchecked: 3]
100+
_blackHole(value)
101+
}
102+
103+
BoundsCheckTraps.test("ArrayUncheckedBounds")
104+
.skip(.custom(
105+
{ _isFastAssertConfiguration() || _isReleaseAssertConfiguration() || _isReleaseAssertWithBoundsSafetyConfiguration() },
106+
reason: "this trap is not guaranteed to happen"))
107+
.code {
108+
expectCrashLater()
109+
var array = [1, 2]
110+
array.append(3)
111+
let value = array[uncheckedBounds: 3..<4]
112+
_blackHole(value)
113+
}
114+
115+
BoundsCheckTraps.test("ContiguousArrayUnchecked")
116+
.skip(.custom(
117+
{ _isFastAssertConfiguration() || _isReleaseAssertConfiguration() || _isReleaseAssertWithBoundsSafetyConfiguration() },
118+
reason: "this trap is not guaranteed to happen"))
119+
.code {
120+
expectCrashLater()
121+
var array: ContiguousArray = [1, 2]
122+
array.append(3)
123+
let value = array[unchecked: 3]
124+
_blackHole(value)
125+
}
126+
127+
BoundsCheckTraps.test("ContiguousArrayUncheckedBounds")
128+
.skip(.custom(
129+
{ _isFastAssertConfiguration() || _isReleaseAssertConfiguration() || _isReleaseAssertWithBoundsSafetyConfiguration() },
130+
reason: "this trap is not guaranteed to happen"))
131+
.code {
132+
expectCrashLater()
133+
var array: ContiguousArray = [1, 2]
134+
array.append(3)
135+
let value = array[uncheckedBounds: 3..<4]
136+
_blackHole(value)
137+
}
138+
91139
runAllTests()

0 commit comments

Comments
 (0)
Please sign in to comment.