@@ -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 $(
0 commit comments