Skip to content

Commit 4e61394

Browse files
committed
[stdlib] Dictionary: Add unsafe bulk-loading initializer
Interface inspired by this Array pitch: https://forums.swift.org/t/array-initializer-with-access-to-uninitialized-buffer/13689
1 parent ef392c1 commit 4e61394

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed

stdlib/public/core/DictionaryBuilder.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,85 @@ struct _DictionaryBuilder<Key: Hashable, Value> {
4242
return Dictionary(_native: _target)
4343
}
4444
}
45+
46+
extension Dictionary {
47+
/// Creates a new dictionary with the specified capacity, then calls the given
48+
/// closure to initialize its contents.
49+
///
50+
/// Foundation uses this initializer to bridge the contents of an NSDictionary
51+
/// instance without allocating a pair of intermediary buffers. Pass the
52+
/// required capacity and a closure that can intialize the dictionary's
53+
/// elements. The closure must return `c`, the number of initialized elements
54+
/// in both buffers, such that the elements in the range `0..<c` are
55+
/// initialized and the elements in the range `c..<capacity` are
56+
/// uninitialized. The resulting dictionary has a `count` equal to `c`.
57+
///
58+
/// The closure must not add any duplicate keys to the keys buffer. The
59+
/// buffers passed to the closure are only valid for the duration of the call.
60+
/// After the closure returns, this initializer moves all initialized elements
61+
/// into their correct buckets.
62+
///
63+
/// - Parameters:
64+
/// - capacity: The capacity of the new dictionary.
65+
/// - body: A closure that can initialize the dictionary's elements. This
66+
/// closure must return the count of the initialized elements, starting at
67+
/// the beginning of the buffer.
68+
@inlinable
69+
public // SPI(Foundation)
70+
init(
71+
_unsafeUninitializedCapacity capacity: Int,
72+
initializingWith initializer: (
73+
_ keys: UnsafeMutableBufferPointer<Key>,
74+
_ values: UnsafeMutableBufferPointer<Value>,
75+
_ initializedCount: inout Int
76+
) -> Void
77+
) {
78+
let native = _NativeDictionary<Key, Value>(capacity: capacity)
79+
let keys = UnsafeMutableBufferPointer<Key>(
80+
start: native._keys,
81+
count: capacity)
82+
let values = UnsafeMutableBufferPointer<Value>(
83+
start: native._values,
84+
count: capacity)
85+
var count = 0
86+
initializer(keys, values, &count)
87+
_precondition(count >= 0 && count <= capacity)
88+
89+
// Hash elements into the correct buckets.
90+
var bucket = _HashTable.Bucket(offset: 0)
91+
while bucket.offset < count {
92+
if native.hashTable._isOccupied(bucket) {
93+
// We've moved an element here in a previous iteration.
94+
bucket.offset += 1
95+
continue
96+
}
97+
let hashValue = native.hashValue(for: native._keys[bucket.offset])
98+
let target: _HashTable.Bucket
99+
if _isDebugAssertConfiguration() {
100+
let (b, found) = native.find(
101+
native._keys[bucket.offset],
102+
hashValue: hashValue)
103+
guard !found else {
104+
_preconditionFailure("Duplicate keys found")
105+
}
106+
native.hashTable.insert(b)
107+
target = b
108+
} else {
109+
target = native.hashTable.insertNew(hashValue: hashValue)
110+
}
111+
if target < bucket || target.offset >= count {
112+
// Target bucket is uninitialized
113+
native.moveEntry(from: bucket, to: target)
114+
bucket.offset += 1
115+
} else if target == bucket {
116+
// Already in place
117+
bucket.offset += 1
118+
} else {
119+
// Swap into place, then try again with the swapped-in value.
120+
native.swapEntry(target, with: bucket)
121+
}
122+
}
123+
native._storage._count = count
124+
self = Dictionary(_native: native)
125+
}
126+
}

stdlib/public/core/NativeDictionary.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,11 +619,22 @@ extension _NativeDictionary: _HashTableDelegate {
619619
@inlinable
620620
@inline(__always)
621621
internal func moveEntry(from source: Bucket, to target: Bucket) {
622+
_internalInvariant(hashTable.isValid(source))
623+
_internalInvariant(hashTable.isValid(target))
622624
(_keys + target.offset)
623625
.moveInitialize(from: _keys + source.offset, count: 1)
624626
(_values + target.offset)
625627
.moveInitialize(from: _values + source.offset, count: 1)
626628
}
629+
630+
@inlinable
631+
@inline(__always)
632+
internal func swapEntry(_ left: Bucket, with right: Bucket) {
633+
_internalInvariant(hashTable.isValid(left))
634+
_internalInvariant(hashTable.isValid(right))
635+
swap(&_keys[left.offset], &_keys[right.offset])
636+
swap(&_values[left.offset], &_values[right.offset])
637+
}
627638
}
628639

629640
extension _NativeDictionary { // Deletion

validation-test/stdlib/Dictionary.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5547,6 +5547,32 @@ DictionaryTestSuite.test("IndexValidation.RemoveAt.AfterGrow") {
55475547
d.remove(at: i)
55485548
}
55495549

5550+
DictionaryTestSuite.test("BulkLoadingInitializer") {
5551+
for c in [0, 1, 2, 3, 5, 10, 25, 150] {
5552+
let d1 = Dictionary<TestKeyTy, TestEquatableValueTy>(
5553+
_unsafeUninitializedCapacity: c
5554+
) { keys, values, count in
5555+
let k = keys.baseAddress!
5556+
let v = values.baseAddress!
5557+
for i in 0 ..< c {
5558+
(k + i).initialize(to: TestKeyTy(i))
5559+
(v + i).initialize(to: TestEquatableValueTy(i))
5560+
count += 1
5561+
}
5562+
}
5563+
5564+
let d2 = Dictionary(
5565+
uniqueKeysWithValues: (0..<c).map {
5566+
(TestKeyTy($0), TestEquatableValueTy($0))
5567+
})
5568+
5569+
for i in 0 ..< c {
5570+
expectEqual(TestEquatableValueTy(i), d1[TestKeyTy(i)])
5571+
}
5572+
expectEqual(d1, d2)
5573+
}
5574+
}
5575+
55505576
DictionaryTestSuite.setUp {
55515577
#if _runtime(_ObjC)
55525578
// Exercise ARC's autoreleased return value optimization in Foundation.

0 commit comments

Comments
 (0)