Skip to content

Commit 19e9b7c

Browse files
authored
Preallocate dictionary storage in dict.fromkeys (#8874)
Assisted-by: Codex:GPT-6
1 parent af37a76 commit 19e9b7c

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

‎crates/vm/src/builtins/dict.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ impl PyDict {
411411
match d.downcast_exact::<Self>(vm) {
412412
Ok(pydict) => {
413413
if let Some(keys) = Self::fromkeys_known_hashes(iterable.as_object(), vm) {
414+
if class.is(vm.ctx.types.dict_type) {
415+
pydict.entries.reserve_for_empty(keys.len());
416+
}
414417
for (key, hash) in keys {
415418
pydict
416419
.entries

‎crates/vm/src/dict_inner.rs‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,35 @@ impl<T: Clone> Dict<T> {
527527
self.inner.write()
528528
}
529529

530+
/// Reserve space for known keys without changing an existing key layout.
531+
/// Only use on an unpublished dictionary: resizing must not race with a
532+
/// lookup that has already probed the old index table.
533+
pub(crate) fn reserve_for_empty(&self, capacity: usize) {
534+
if capacity == 0 {
535+
return;
536+
}
537+
let Some(index_capacity) = capacity
538+
.checked_add(capacity.div_ceil(2))
539+
.and_then(usize::checked_next_power_of_two)
540+
else {
541+
return;
542+
};
543+
let mut inner = self.write();
544+
if inner.filled != 0 || !inner.entries.is_empty() {
545+
return;
546+
}
547+
// Keep the usual incremental growth path if preallocation fails.
548+
if inner.entries.try_reserve_exact(capacity).is_err() {
549+
return;
550+
}
551+
let additional = index_capacity.saturating_sub(inner.indices.len());
552+
if additional != 0 && inner.indices.try_reserve_exact(additional).is_ok() {
553+
// Every existing index is FREE, so extending needs no rehash and
554+
// leaves the empty key layout and its version unchanged.
555+
inner.indices.resize(index_capacity, IndexEntry::FREE);
556+
}
557+
}
558+
530559
/// Store a key
531560
pub(crate) fn insert<K>(&self, vm: &VirtualMachine, key: &K, value: T) -> PyResult<()>
532561
where
@@ -1870,6 +1899,45 @@ mod tests {
18701899
})
18711900
}
18721901

1902+
#[test]
1903+
fn reserve_empty_avoids_growth() {
1904+
Interpreter::without_stdlib(Default::default()).enter(|vm| {
1905+
let dict = Dict::default();
1906+
dict.reserve_for_empty(usize::MAX);
1907+
assert_eq!(dict.read().indices.len(), 8);
1908+
assert_eq!(dict.read().entries.capacity(), 0);
1909+
1910+
let version = dict.assign_keys_version();
1911+
let count = 1000;
1912+
dict.reserve_for_empty(count);
1913+
assert_eq!(dict.keys_version(), version);
1914+
let capacity = {
1915+
let inner = dict.read();
1916+
(inner.indices.len(), inner.entries.capacity())
1917+
};
1918+
for key in 0..count {
1919+
dict.insert(vm, &key, vm.ctx.none()).unwrap();
1920+
let inner = dict.read();
1921+
assert_eq!((inner.indices.len(), inner.entries.capacity()), capacity);
1922+
}
1923+
assert_eq!(dict.len(), count);
1924+
assert!(dict.contains(vm, &(count - 1)).unwrap());
1925+
1926+
// Existing and tombstoned layouts are left intact.
1927+
dict.reserve_for_empty(count * 4);
1928+
{
1929+
let inner = dict.read();
1930+
assert_eq!((inner.indices.len(), inner.entries.capacity()), capacity);
1931+
}
1932+
for key in 0..count {
1933+
dict.delete(vm, &key).unwrap();
1934+
}
1935+
let old_size = dict.read().size();
1936+
dict.reserve_for_empty(count * 4);
1937+
assert_eq!(dict.read().size(), old_size);
1938+
})
1939+
}
1940+
18731941
macro_rules! hash_tests {
18741942
($($name:ident: $example_hash:expr,)*) => {
18751943
$(

‎extra_tests/snippets/builtin_dict.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,25 @@ class MyClass:
291291
assert {1: None, "b": None} == dict.fromkeys([1, "b"])
292292
assert {1: 0, "b": 0} == dict.fromkeys([1, "b"], 0)
293293

294+
for source in ({i: -1 for i in range(128)}, set(range(128)), frozenset(range(128))):
295+
shared_value = []
296+
result = dict.fromkeys(source, shared_value)
297+
assert list(result) == list(source)
298+
assert all(value is shared_value for value in result.values())
299+
300+
301+
class UnsizedFromKeys:
302+
def __iter__(self):
303+
return iter((1, 1, 2))
304+
305+
def __len__(self):
306+
raise AssertionError("fromkeys must not request a length hint")
307+
308+
__length_hint__ = __len__
309+
310+
311+
assert dict.fromkeys(UnsizedFromKeys()) == {1: None, 2: None}
312+
294313
x = {"a": 1, "b": 1, "c": 1}
295314
y = {"b": 2, "c": 2, "d": 2}
296315
z = {"c": 3, "d": 3, "e": 3}

0 commit comments

Comments
 (0)