Skip to content

Commit 715ccd7

Browse files
committed
[stdlib] Implement the new _modify accessor in Dictionary.subscript
This enables in-place mutations of the key-based subscript. These get rid of one of two full hash table lookup operations (including hashing the key) relative to the getter+setter approach. They can also eliminate COW copies in code like this: dictionary[foo]?.append(bar) (Admittedly this isn’t a very useful example.)
1 parent 99d0147 commit 715ccd7

File tree

1 file changed

+105
-3
lines changed

1 file changed

+105
-3
lines changed

stdlib/public/core/Dictionary.swift

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,60 @@ extension Dictionary {
785785
removeValue(forKey: key)
786786
}
787787
}
788+
_modify {
789+
// FIXME: This code should be moved to _variant, with Dictionary.subscript
790+
// yielding `&_variant[key]`.
791+
792+
// Look up (empty or occupied) bucket corresponding to the given key.
793+
let isUnique = _variant.isUniquelyReferenced()
794+
var idealBucket = _variant.asNative._bucket(key)
795+
var (pos, found) = _variant.asNative._find(key, startBucket: idealBucket)
796+
797+
// Prepare storage.
798+
// If `key` isn't in the dictionary yet, assume that this access will end
799+
// up inserting it. Otherwise this can only be a removal or an in-place
800+
// mutation. (If we guess wrong, we might needlessly rehash; that's fine.)
801+
let (_, rehashed) = _variant.ensureUniqueNative(
802+
withCapacity: self.capacity + (found ? 0 : 1),
803+
isUnique: isUnique)
804+
// FIXME: we should be able to make this a let; however, some of the
805+
// low-level operations below are (incorrectly) marked as mutating.
806+
var native = _variant.asNative
807+
if rehashed {
808+
// Key needs to be hashed again if storage has been resized.
809+
_sanityCheck(!found)
810+
idealBucket = native._bucket(key)
811+
(pos, found) = native._find(key, startBucket: idealBucket)
812+
_sanityCheck(!found)
813+
}
814+
// FIXME: Mark this entry as being modified in hash table metadata
815+
// so that lldb can recognize it's not valid.
816+
817+
// Move the old value (if any) out of storage, wrapping it into an
818+
// optional before yielding it.
819+
var value: Value? = found ? native.moveValue(at: pos.bucket) : nil
820+
yield &value
821+
822+
// Value is now potentially different. Check which one of the four
823+
// possible cases apply.
824+
switch (found, value != nil) {
825+
case (true, true): // Mutation
826+
// Initialize storage to new value.
827+
(native.values + pos.bucket).initialize(to: value!)
828+
case (true, false): // Removal
829+
// We've already removed the value; deinitialize and remove the key too.
830+
native.destroyHalfEntry(at: pos.bucket)
831+
native._deleteDestroyed(idealBucket: idealBucket, bucket: pos.bucket)
832+
case (false, true): // Insertion
833+
// Insert the new entry at the correct place.
834+
// We've already ensured we have enough capacity.
835+
native.initializeKey(key, value: value!, at: pos.bucket)
836+
native.count += 1
837+
case (false, false): // Noop
838+
// Easy!
839+
break
840+
}
841+
}
788842
}
789843
}
790844

@@ -2369,6 +2423,21 @@ internal struct _NativeDictionary<Key, Value> {
23692423
_storage.initializedEntries[i] = false
23702424
}
23712425

2426+
@inlinable
2427+
internal func moveValue(at bucket: Int) -> Value {
2428+
defer { _fixLifetime(self) }
2429+
return (values + bucket).move()
2430+
}
2431+
2432+
// This assumes the value is already deinitialized.
2433+
@inlinable
2434+
internal func destroyHalfEntry(at bucket: Int) {
2435+
_sanityCheck(isInitializedEntry(at: bucket))
2436+
defer { _fixLifetime(self) }
2437+
(keys + bucket).deinitialize(count: 1)
2438+
_storage.initializedEntries[bucket] = false
2439+
}
2440+
23722441
@usableFromInline @_transparent
23732442
internal func initializeKey(_ k: Key, value v: Value, at i: Int) {
23742443
_sanityCheck(!isInitializedEntry(at: i))
@@ -2633,6 +2702,13 @@ extension _NativeDictionary where Key: Hashable {
26332702

26342703
// remove the element
26352704
destroyEntry(at: bucket)
2705+
_deleteDestroyed(idealBucket: idealBucket, bucket: bucket)
2706+
}
2707+
2708+
@inlinable // FIXME(sil-serialize-all)
2709+
internal mutating func _deleteDestroyed(idealBucket: Int, bucket: Int) {
2710+
_sanityCheck(!isInitializedEntry(at: bucket), "expected initialized entry")
2711+
26362712
self.count -= 1
26372713

26382714
// If we've put a hole in a chain of contiguous elements, some
@@ -3283,14 +3359,26 @@ extension Dictionary._Variant: _DictionaryBuffer {
32833359
internal mutating func _ensureUniqueNative(
32843360
withBucketCount desiredBucketCount: Int
32853361
) -> (reallocated: Bool, capacityChanged: Bool) {
3286-
let oldBucketCount = asNative.bucketCount
32873362
let isUnique = isUniquelyReferenced()
3363+
return _ensureUniqueNative(
3364+
withBucketCount: desiredBucketCount,
3365+
isUnique: isUnique)
3366+
}
3367+
3368+
@inline(__always)
3369+
@inlinable // FIXME(sil-serialize-all)
3370+
internal mutating func _ensureUniqueNative(
3371+
withBucketCount desiredBucketCount: Int,
3372+
isUnique: Bool
3373+
) -> (reallocated: Bool, capacityChanged: Bool) {
3374+
let oldBucketCount = asNative.bucketCount
32883375
if oldBucketCount >= desiredBucketCount && isUnique {
32893376
return (reallocated: false, capacityChanged: false)
32903377
}
32913378

32923379
let oldDictionary = asNative
3293-
var newDictionary = _NativeDictionary<Key, Value>(bucketCount: desiredBucketCount)
3380+
var newDictionary = _NativeDictionary<Key, Value>(
3381+
bucketCount: desiredBucketCount)
32943382
let newBucketCount = newDictionary.bucketCount
32953383
for i in 0..<oldBucketCount {
32963384
if oldDictionary.isInitializedEntry(at: i) {
@@ -3309,7 +3397,9 @@ extension Dictionary._Variant: _DictionaryBuffer {
33093397
newDictionary.count = oldDictionary.count
33103398

33113399
self = .native(newDictionary)
3312-
return (reallocated: true, capacityChanged: oldBucketCount != newBucketCount)
3400+
return (
3401+
reallocated: true,
3402+
capacityChanged: oldBucketCount != newBucketCount)
33133403
}
33143404

33153405
@inline(__always)
@@ -3323,6 +3413,18 @@ extension Dictionary._Variant: _DictionaryBuffer {
33233413
return ensureUniqueNative(withBucketCount: bucketCount)
33243414
}
33253415

3416+
@inline(__always)
3417+
@inlinable // FIXME(sil-serialize-all)
3418+
internal mutating func ensureUniqueNative(
3419+
withCapacity minimumCapacity: Int,
3420+
isUnique: Bool
3421+
) -> (reallocated: Bool, capacityChanged: Bool) {
3422+
let bucketCount = _NativeDictionary<Key, Value>.bucketCount(
3423+
forCapacity: minimumCapacity,
3424+
maxLoadFactorInverse: _hashContainerDefaultMaxLoadFactorInverse)
3425+
return _ensureUniqueNative(withBucketCount: bucketCount, isUnique: isUnique)
3426+
}
3427+
33263428
/// Ensure this we hold a unique reference to a native dictionary
33273429
/// having at least `minimumCapacity` elements.
33283430
@inlinable // FIXME(sil-serialize-all)

0 commit comments

Comments
 (0)