Skip to content

Commit 2e8ae40

Browse files
authored
Merge pull request #63592 from lorentey/rework-string-breadcrumbs
[stdlib] Rework String breadcrumbs initialization/loading
2 parents 2deaf1d + 11f3af0 commit 2e8ae40

File tree

5 files changed

+191
-35
lines changed

5 files changed

+191
-35
lines changed

stdlib/public/core/Runtime.swift

+43-2
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,19 @@ func _stdlib_atomicInitializeARCRef(
131131
object target: UnsafeMutablePointer<AnyObject?>,
132132
desired: AnyObject
133133
) -> Bool {
134+
// Note: this assumes that AnyObject? is layout-compatible with a RawPointer
135+
// that simply points to the same memory.
134136
var expected: UnsafeRawPointer?
135-
let desiredPtr = Unmanaged.passRetained(desired).toOpaque()
137+
let unmanaged = Unmanaged.passRetained(desired)
138+
let desiredPtr = unmanaged.toOpaque()
136139
let rawTarget = UnsafeMutableRawPointer(target).assumingMemoryBound(
137140
to: Optional<UnsafeRawPointer>.self)
138141
let wonRace = _stdlib_atomicCompareExchangeStrongPtr(
139142
object: rawTarget, expected: &expected, desired: desiredPtr)
140143
if !wonRace {
141144
// Some other thread initialized the value. Balance the retain that we
142145
// performed on 'desired'.
143-
Unmanaged.passUnretained(desired).release()
146+
unmanaged.release()
144147
}
145148
return wonRace
146149
}
@@ -157,6 +160,44 @@ func _stdlib_atomicLoadARCRef(
157160
return nil
158161
}
159162

163+
@_transparent
164+
@_alwaysEmitIntoClient
165+
@discardableResult
166+
public func _stdlib_atomicAcquiringInitializeARCRef<T: AnyObject>(
167+
object target: UnsafeMutablePointer<T?>,
168+
desired: __owned T
169+
) -> Unmanaged<T> {
170+
// Note: this assumes that AnyObject? is layout-compatible with a RawPointer
171+
// that simply points to the same memory, and that `nil` is represented by an
172+
// all-zero bit pattern.
173+
let unmanaged = Unmanaged.passRetained(desired)
174+
let desiredPtr = unmanaged.toOpaque()
175+
176+
let (value, won) = Builtin.cmpxchg_acqrel_acquire_Word(
177+
target._rawValue,
178+
0._builtinWordValue,
179+
Builtin.ptrtoint_Word(desiredPtr._rawValue))
180+
181+
if Bool(won) { return unmanaged }
182+
183+
// Some other thread initialized the value before us. Balance the retain that
184+
// we performed on 'desired', and return what we loaded.
185+
unmanaged.release()
186+
let ptr = UnsafeRawPointer(Builtin.inttoptr_Word(value))
187+
return Unmanaged<T>.fromOpaque(ptr)
188+
}
189+
190+
@_alwaysEmitIntoClient
191+
@_transparent
192+
public func _stdlib_atomicAcquiringLoadARCRef<T: AnyObject>(
193+
object target: UnsafeMutablePointer<T?>
194+
) -> Unmanaged<T>? {
195+
let value = Builtin.atomicload_acquire_Word(target._rawValue)
196+
if Int(value) == 0 { return nil }
197+
let opaque = UnsafeRawPointer(Builtin.inttoptr_Word(value))
198+
return Unmanaged<T>.fromOpaque(opaque)
199+
}
200+
160201
//===----------------------------------------------------------------------===//
161202
// Conversion of primitive types to `String`
162203
//===----------------------------------------------------------------------===//

stdlib/public/core/StringStorage.swift

+16-19
Original file line numberDiff line numberDiff line change
@@ -719,8 +719,18 @@ extension __SharedStringStorage {
719719

720720
// Get and populate breadcrumbs
721721
extension _StringGuts {
722+
/// Atomically load and return breadcrumbs, populating them if necessary.
723+
///
724+
/// This emits aquire/release barriers to avoid access reordering trouble.
725+
///
726+
/// This returns an unmanaged +0 reference to allow accessing breadcrumbs
727+
/// without incurring retain/release operations.
722728
@_effects(releasenone)
723-
internal func getBreadcrumbsPtr() -> UnsafePointer<_StringBreadcrumbs> {
729+
internal func loadUnmanagedBreadcrumbs() -> Unmanaged<_StringBreadcrumbs> {
730+
// FIXME: Returning Unmanaged emulates the original implementation (that
731+
// used to return a nonatomic pointer), but it may be an unnecessary
732+
// complication. (UTF-16 transcoding seems expensive enough not to worry
733+
// about retain overhead.)
724734
_internalInvariant(hasBreadcrumbs)
725735

726736
let mutPtr: UnsafeMutablePointer<_StringBreadcrumbs?>
@@ -731,25 +741,12 @@ extension _StringGuts {
731741
Builtin.addressof(&_object.sharedStorage._breadcrumbs))
732742
}
733743

734-
if _slowPath(mutPtr.pointee == nil) {
735-
populateBreadcrumbs(mutPtr)
744+
if let breadcrumbs = _stdlib_atomicAcquiringLoadARCRef(object: mutPtr) {
745+
return breadcrumbs
736746
}
737-
738-
_internalInvariant(mutPtr.pointee != nil)
739-
// assuming optional class reference and class reference can alias
740-
return UnsafeRawPointer(mutPtr).assumingMemoryBound(to: _StringBreadcrumbs.self)
741-
}
742-
743-
@inline(never) // slow-path
744-
@_effects(releasenone)
745-
internal func populateBreadcrumbs(
746-
_ mutPtr: UnsafeMutablePointer<_StringBreadcrumbs?>
747-
) {
748-
// Thread-safe compare-and-swap
749-
let crumbs = _StringBreadcrumbs(String(self))
750-
_stdlib_atomicInitializeARCRef(
751-
object: UnsafeMutableRawPointer(mutPtr).assumingMemoryBound(to: Optional<AnyObject>.self),
752-
desired: crumbs)
747+
let desired = _StringBreadcrumbs(String(self))
748+
return _stdlib_atomicAcquiringInitializeARCRef(
749+
object: mutPtr, desired: desired)
753750
}
754751
}
755752

stdlib/public/core/StringStorageBridge.swift

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ extension _AbstractStringStorage {
4848
) {
4949
_precondition(aRange.location >= 0 && aRange.length >= 0,
5050
"Range out of bounds")
51+
// Note: `count` is counting UTF-8 code units, while `aRange` is measured in
52+
// UTF-16 offsets. This precondition is a necessary, but not sufficient test
53+
// for validity. (More precise checks are done in UTF16View._nativeCopy.)
5154
_precondition(aRange.location + aRange.length <= Int(count),
5255
"Range out of bounds")
5356

stdlib/public/core/StringUTF16View.swift

+25-12
Original file line numberDiff line numberDiff line change
@@ -852,16 +852,20 @@ extension String.UTF16View {
852852
return _utf16Distance(from: startIndex, to: idx)
853853
}
854854

855+
let breadcrumbs = _guts.loadUnmanagedBreadcrumbs()
856+
855857
// Simple and common: endIndex aka `length`.
856-
let breadcrumbsPtr = _guts.getBreadcrumbsPtr()
857-
if idx == endIndex { return breadcrumbsPtr.pointee.utf16Length }
858+
if idx == endIndex {
859+
return breadcrumbs._withUnsafeGuaranteedRef { $0.utf16Length }
860+
}
858861

859-
// Otherwise, find the nearest lower-bound breadcrumb and count from there
860-
// FIXME: Starting from the upper-bound crumb when that is closer would cut
861-
// the average cost of the subsequent iteration by 50%.
862-
let (crumb, crumbOffset) = breadcrumbsPtr.pointee.getBreadcrumb(
863-
forIndex: idx)
864-
return crumbOffset + _utf16Distance(from: crumb, to: idx)
862+
return breadcrumbs._withUnsafeGuaranteedRef { crumbs in
863+
// Otherwise, find the nearest lower-bound breadcrumb and count from there
864+
// FIXME: Starting from the upper-bound crumb when that is closer would
865+
// cut the average cost of the subsequent iteration by 50%.
866+
let (crumb, crumbOffset) = crumbs.getBreadcrumb(forIndex: idx)
867+
return crumbOffset + _utf16Distance(from: crumb, to: idx)
868+
}
865869
}
866870

867871
/// Return the index at the given UTF-16 offset, measured from the
@@ -892,14 +896,17 @@ extension String.UTF16View {
892896
}
893897

894898
// Simple and common: endIndex aka `length`.
895-
let breadcrumbsPtr = _guts.getBreadcrumbsPtr()
896-
if offset == breadcrumbsPtr.pointee.utf16Length { return endIndex }
899+
let breadcrumbs = _guts.loadUnmanagedBreadcrumbs()
900+
let utf16Count = breadcrumbs._withUnsafeGuaranteedRef { $0.utf16Length }
901+
if offset == utf16Count { return endIndex }
897902

898903
// Otherwise, find the nearest lower-bound breadcrumb and advance that
899904
// FIXME: Starting from the upper-bound crumb when that is closer would cut
900905
// the average cost of the subsequent iteration by 50%.
901-
let (crumb, remaining) = breadcrumbsPtr.pointee.getBreadcrumb(
902-
forOffset: offset)
906+
let (crumb, remaining) = breadcrumbs._withUnsafeGuaranteedRef {
907+
$0.getBreadcrumb(forOffset: offset)
908+
}
909+
_internalInvariant(crumb._canBeUTF8 && crumb._encodedOffset <= _guts.count)
903910
if remaining == 0 { return crumb }
904911

905912
return _guts.withFastUTF8 { utf8 in
@@ -981,6 +988,8 @@ extension String.UTF16View {
981988
if _slowPath(range.lowerBound.transcodedOffset != 0) {
982989
_internalInvariant(range.lowerBound.transcodedOffset == 1)
983990
let (scalar, len) = _decodeScalar(utf8, startingAt: readIdx)
991+
// Note: this is intentionally not using the _unchecked subscript.
992+
// (We rely on debug assertions to catch out of bounds access.)
984993
buffer[writeIdx] = scalar.utf16[1]
985994
readIdx &+= len
986995
writeIdx &+= 1
@@ -993,6 +1002,8 @@ extension String.UTF16View {
9931002
readIdx &+= len
9941003
writeIdx &+= 1
9951004
if _slowPath(scalar.utf16.count == 2) {
1005+
// Note: this is intentionally not using the _unchecked subscript.
1006+
// (We rely on debug assertions to catch out of bounds access.)
9961007
buffer[writeIdx] = scalar.utf16[1]
9971008
writeIdx &+= 1
9981009
}
@@ -1004,6 +1015,8 @@ extension String.UTF16View {
10041015
let (scalar, _) = _decodeScalar(utf8, startingAt: readIdx)
10051016
_internalInvariant(scalar.utf16.count == 2)
10061017

1018+
// Note: this is intentionally not using the _unchecked subscript.
1019+
// (We rely on debug assertions to catch out of bounds access.)
10071020
buffer[writeIdx] = scalar.utf16[0]
10081021
writeIdx &+= 1
10091022
}

validation-test/StdlibUnittest/AtomicInt.swift

+104-2
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,103 @@ struct AtomicInitializeARCRefRaceTest : RaceTestWithPerTrialData {
795795
}
796796
}
797797

798+
struct AtomicAcquiringARCRefRaceTest: RaceTestWithPerTrialData {
799+
typealias DummyObject = AtomicInitializeARCRefRaceTest.DummyObject
800+
801+
class RaceData {
802+
var _atomicReference: DummyObject? = nil
803+
804+
var atomicReferencePtr: UnsafeMutablePointer<DummyObject?> {
805+
_getUnsafePointerToStoredProperties(self).assumingMemoryBound(
806+
to: Optional<DummyObject>.self)
807+
}
808+
809+
init() {}
810+
}
811+
812+
typealias ThreadLocalData = _stdlib_ShardedAtomicCounter.PRNG
813+
typealias Observation = Observation4UInt
814+
815+
func makeRaceData() -> RaceData {
816+
RaceData()
817+
}
818+
819+
func makeThreadLocalData() -> ThreadLocalData {
820+
ThreadLocalData()
821+
}
822+
823+
func thread1(
824+
_ raceData: RaceData, _ threadLocalData: inout ThreadLocalData
825+
) -> Observation {
826+
var observation = Observation4UInt(0, 0, 0, 0)
827+
let initializerDestroyed = HeapBool(false)
828+
do {
829+
let object = DummyObject(
830+
destroyedFlag: initializerDestroyed,
831+
randomInt: threadLocalData.randomInt())
832+
let value = Unmanaged.passUnretained(object)
833+
let result = _stdlib_atomicAcquiringInitializeARCRef(
834+
object: raceData.atomicReferencePtr, desired: object)
835+
observation.data1 = (result.toOpaque() == value.toOpaque() ? 1 : 0)
836+
if let loaded =
837+
_stdlib_atomicAcquiringLoadARCRef(object: raceData.atomicReferencePtr) {
838+
observation.data2 = UInt(bitPattern: loaded.toOpaque())
839+
observation.data3 = loaded._withUnsafeGuaranteedRef { $0.payload }
840+
}
841+
}
842+
observation.data4 = initializerDestroyed.value ? 1 : 0
843+
return observation
844+
}
845+
846+
func evaluateObservations(
847+
_ observations: [Observation],
848+
_ sink: (RaceTestObservationEvaluation) -> Void
849+
) {
850+
let ref = observations[0].data2
851+
if observations.contains(where: { $0.data2 != ref }) {
852+
for observation in observations {
853+
sink(.failureInteresting("mismatched reference, expected \(ref): \(observation)"))
854+
}
855+
return
856+
}
857+
if observations.contains(where: { $0.data3 != 0x12345678 }) {
858+
for observation in observations {
859+
sink(.failureInteresting("wrong data: \(observation)"))
860+
}
861+
return
862+
}
863+
864+
var wonRace = 0
865+
var lostRace = 0
866+
for observation in observations {
867+
switch (observation.data1, observation.data4) {
868+
case (1, 0):
869+
// Won race, value not destroyed.
870+
wonRace += 1
871+
case (0, 1):
872+
// Lost race, value destroyed.
873+
lostRace += 1
874+
default:
875+
sink(.failureInteresting(String(describing: observation)))
876+
}
877+
}
878+
if wonRace != 1 {
879+
for observation in observations {
880+
sink(.failureInteresting("zero or more than one thread won race: \(observation)"))
881+
}
882+
return
883+
}
884+
if lostRace < 1 {
885+
for observation in observations {
886+
sink(.failureInteresting("no thread lost race: \(observation)"))
887+
}
888+
return
889+
}
890+
891+
sink(.pass)
892+
}
893+
}
894+
798895
var AtomicIntTestSuite = TestSuite("AtomicInt")
799896

800897
AtomicIntTestSuite.test("fetchAndAdd/1") {
@@ -841,11 +938,16 @@ AtomicIntTestSuite.test("fetchAndXor/1") {
841938

842939
var AtomicARCRefTestSuite = TestSuite("AtomicARCRef")
843940

844-
AtomicARCRefTestSuite.test("initialize,load") {
941+
AtomicARCRefTestSuite.test("seqcst_initialize,load") {
845942
runRaceTest(AtomicInitializeARCRefRaceTest.self,
846943
operations: 25600, timeoutInSeconds: 60)
847944
expectEqual(0, dummyObjectCount.getSum())
848945
}
849946

850-
runAllTests()
947+
AtomicARCRefTestSuite.test("acquire_initialize,load") {
948+
runRaceTest(AtomicAcquiringARCRefRaceTest.self,
949+
operations: 25600, timeoutInSeconds: 60)
950+
expectEqual(0, dummyObjectCount.getSum())
951+
}
851952

953+
runAllTests()

0 commit comments

Comments
 (0)