44use crate :: LinearAllocator ;
55use crate :: { AllocError , Allocator } ;
66use core:: alloc:: Layout ;
7- use core:: cell:: UnsafeCell ;
7+ use core:: cell:: { Cell , UnsafeCell } ;
88use core:: mem:: size_of;
99use core:: ptr:: NonNull ;
1010
@@ -17,11 +17,16 @@ use core::ptr::NonNull;
1717/// [ChainAllocator] creates a new [LinearAllocator] when the current one
1818/// doesn't have enough space for the requested allocation, and then links the
1919/// new [LinearAllocator] to the previous one, creating a chain. This is where
20- /// its name comes from.
20+ /// its name comes from. Each successful growth doubles the target chunk size
21+ /// up to a cap, so small arenas retain a low initial footprint while larger
22+ /// workloads quickly converge to larger chunks.
2123pub struct ChainAllocator < A : Allocator + Clone > {
2224 top : UnsafeCell < ChainNodePtr < A > > ,
23- /// The size hint for the linear allocator's chunk.
24- node_size : usize ,
25+ /// The size hint for the next linear allocator chunk.
26+ node_size : Cell < usize > ,
27+ /// The maximum size hint used for routine geometric growth. Individual
28+ /// oversized allocations can still request larger chunks.
29+ max_node_size : usize ,
2530 allocator : A ,
2631}
2732
@@ -87,38 +92,89 @@ impl<A: Allocator + Clone> ChainAllocator<A> {
8792 /// is worth it. This is somewhat arbitrarily chosen at the moment.
8893 const MIN_NODE_SIZE : usize = 4 * Self :: CHAIN_NODE_OVERHEAD ;
8994
90- /// Creates a new [ChainAllocator]. The `chunk_size_hint` is used as a
91- /// size hint when creating new chunks of the chain. Note that the
95+ /// Default cap for routine geometric growth. This preserves the historical
96+ /// chunk size used by profiling dictionaries while allowing smaller initial
97+ /// chunks to ramp up quickly.
98+ const DEFAULT_MAX_NODE_SIZE : usize = 1024 * 1024 ;
99+
100+ const fn normalize_node_size ( size : usize ) -> usize {
101+ if size < Self :: MIN_NODE_SIZE {
102+ Self :: MIN_NODE_SIZE
103+ } else {
104+ size
105+ }
106+ }
107+
108+ /// Creates a new [ChainAllocator]. The `chunk_size_hint` is used as the
109+ /// initial size hint for chunks of the chain. Routine growth doubles the
110+ /// chunk size up to at least `Self::DEFAULT_MAX_NODE_SIZE`. Note that the
92111 /// [ChainAllocator] will use some bytes at the beginning of each chunk of
93112 /// the chain. The number of bytes is [Self::CHAIN_NODE_OVERHEAD]. Keep
94113 /// this in mind when sizing your hint if you are trying to be precise,
95114 /// such as making sure a specific object fits.
96115 pub const fn new_in ( chunk_size_hint : usize , allocator : A ) -> Self {
116+ let initial_node_size = Self :: normalize_node_size ( chunk_size_hint) ;
117+ let max_node_size = if initial_node_size < Self :: DEFAULT_MAX_NODE_SIZE {
118+ Self :: DEFAULT_MAX_NODE_SIZE
119+ } else {
120+ initial_node_size
121+ } ;
122+ Self :: new_capped_in ( initial_node_size, max_node_size, allocator)
123+ }
124+
125+ /// Creates a new [ChainAllocator] whose routine growth starts at
126+ /// `chunk_size_hint` and doubles until reaching `max_chunk_size_hint`.
127+ /// Requests larger than the cap are still honored by allocating an
128+ /// oversized chunk for that request.
129+ pub const fn new_capped_in (
130+ chunk_size_hint : usize ,
131+ max_chunk_size_hint : usize ,
132+ allocator : A ,
133+ ) -> Self {
134+ let initial_node_size = Self :: normalize_node_size ( chunk_size_hint) ;
135+ let max_node_size = if max_chunk_size_hint < initial_node_size {
136+ initial_node_size
137+ } else {
138+ max_chunk_size_hint
139+ } ;
97140 Self {
98141 top : UnsafeCell :: new ( ChainNodePtr :: new ( ) ) ,
99- // max is not a const fn, do it manually.
100- node_size : if chunk_size_hint < Self :: MIN_NODE_SIZE {
101- Self :: MIN_NODE_SIZE
102- } else {
103- chunk_size_hint
104- } ,
142+ node_size : Cell :: new ( initial_node_size) ,
143+ max_node_size,
105144 allocator,
106145 }
107146 }
108147
148+ fn next_geometric_node_size ( current : usize , max : usize , align : usize ) -> usize {
149+ if current >= max {
150+ return max;
151+ }
152+
153+ let Some ( doubled) = current. checked_mul ( 2 ) else {
154+ return max;
155+ } ;
156+ let next = if doubled > max { max } else { doubled } ;
157+
158+ if Layout :: from_size_align ( next, align) . is_ok ( ) {
159+ next
160+ } else {
161+ current
162+ }
163+ }
164+
109165 #[ cold]
110166 #[ inline( never) ]
111167 fn grow ( & self , min_size : usize ) -> Result < ( ) , AllocError > {
112168 let top = self . top . get ( ) ;
113169 let chain_layout = Layout :: new :: < ChainNode < A > > ( ) ;
114170
115- let node_size = min_size. max ( self . node_size ) ;
116- let linear = {
117- let layout = Layout :: from_size_align ( node_size , chain_layout . align ( ) )
118- . map_err ( |_| AllocError ) ?
119- . pad_to_align ( ) ;
120- LinearAllocator :: new_in ( layout, self . allocator . clone ( ) ) ?
121- } ;
171+ let node_size = min_size. max ( self . node_size . get ( ) ) ;
172+ let layout = Layout :: from_size_align ( node_size , chain_layout . align ( ) )
173+ . map_err ( |_| AllocError ) ?
174+ . pad_to_align ( ) ;
175+ let next_node_size =
176+ Self :: next_geometric_node_size ( layout. size ( ) , self . max_node_size , chain_layout . align ( ) ) ;
177+ let linear = LinearAllocator :: new_in ( layout , self . allocator . clone ( ) ) ? ;
122178
123179 // This shouldn't fail.
124180 let chain_node_addr = linear
@@ -148,6 +204,7 @@ impl<A: Allocator + Clone> ChainAllocator<A> {
148204 // Additionally, references are always temporary for the top, so this
149205 // write will not violate aliasing rules.
150206 unsafe { self . top . get ( ) . write ( chain_node_ptr) } ;
207+ self . node_size . set ( next_node_size) ;
151208
152209 Ok ( ( ) )
153210 }
@@ -382,6 +439,91 @@ mod tests {
382439 unsafe { allocator. deallocate ( ptr. cast ( ) , layout) } ;
383440 }
384441
442+ #[ test]
443+ fn test_geometric_growth ( ) {
444+ let allocator = ChainAllocator :: new_in ( 4096 , Global ) ;
445+ let layout = Layout :: new :: < u8 > ( ) ;
446+
447+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
448+ let first_reserved = allocator. reserved_bytes ( ) ;
449+ fill_to_capacity ( & allocator) ;
450+
451+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
452+ let second_reserved = allocator. reserved_bytes ( ) ;
453+ let second_chunk = second_reserved - first_reserved;
454+ assert ! (
455+ second_chunk >= first_reserved * 2 ,
456+ "second chunk should grow geometrically: first={first_reserved}, second={second_chunk}"
457+ ) ;
458+ fill_to_capacity ( & allocator) ;
459+
460+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
461+ let third_reserved = allocator. reserved_bytes ( ) ;
462+ let third_chunk = third_reserved - second_reserved;
463+ assert ! (
464+ third_chunk >= second_chunk * 2 ,
465+ "third chunk should grow geometrically: second={second_chunk}, third={third_chunk}"
466+ ) ;
467+ }
468+
469+ #[ test]
470+ fn test_geometric_growth_clamps_to_cap ( ) {
471+ let allocator = ChainAllocator :: new_capped_in ( 4096 , 10 * 1024 , Global ) ;
472+ let layout = Layout :: new :: < u8 > ( ) ;
473+
474+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
475+ let first_reserved = allocator. reserved_bytes ( ) ;
476+ fill_to_capacity ( & allocator) ;
477+
478+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
479+ let second_reserved = allocator. reserved_bytes ( ) ;
480+ fill_to_capacity ( & allocator) ;
481+
482+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
483+ let third_reserved = allocator. reserved_bytes ( ) ;
484+ let third_chunk = third_reserved - second_reserved;
485+
486+ assert ! ( second_reserved - first_reserved >= first_reserved * 2 ) ;
487+ assert ! ( third_chunk >= 10 * 1024 ) ;
488+ assert ! ( third_chunk < ( second_reserved - first_reserved) * 2 ) ;
489+ }
490+
491+ #[ test]
492+ fn test_capped_growth_honors_initial_size_as_minimum_cap ( ) {
493+ let allocator = ChainAllocator :: new_capped_in ( 8192 , 4096 , Global ) ;
494+ let layout = Layout :: new :: < u8 > ( ) ;
495+
496+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
497+ let first_reserved = allocator. reserved_bytes ( ) ;
498+ fill_to_capacity ( & allocator) ;
499+
500+ let _ = allocator. allocate ( layout) . unwrap ( ) ;
501+ let second_reserved = allocator. reserved_bytes ( ) ;
502+ let second_chunk = second_reserved - first_reserved;
503+
504+ assert ! ( second_chunk >= first_reserved) ;
505+ assert ! ( second_chunk < first_reserved * 2 ) ;
506+ }
507+
508+ #[ test]
509+ fn test_next_geometric_node_size_edge_cases ( ) {
510+ assert_eq ! (
511+ 4096 ,
512+ ChainAllocator :: <Global >:: next_geometric_node_size( 8192 , 4096 , 1 )
513+ ) ;
514+ assert_eq ! (
515+ usize :: MAX ,
516+ ChainAllocator :: <Global >:: next_geometric_node_size( usize :: MAX / 2 + 1 , usize :: MAX , 1 )
517+ ) ;
518+
519+ let invalid_layout_size = isize:: MAX as usize + 1 ;
520+ let current = invalid_layout_size / 2 + 1 ;
521+ assert_eq ! (
522+ current,
523+ ChainAllocator :: <Global >:: next_geometric_node_size( current, invalid_layout_size, 1 )
524+ ) ;
525+ }
526+
385527 #[ track_caller]
386528 fn fill_to_capacity < A : Allocator + Clone > ( allocator : & ChainAllocator < A > ) {
387529 let remaining_capacity = allocator. remaining_capacity ( ) ;
@@ -401,8 +543,10 @@ mod tests {
401543
402544 let bool_layout = Layout :: new :: < bool > ( ) ;
403545
546+ const GROWTH_ITERATIONS : usize = 16 ;
547+
404548 // test that it fills to capacity a few times.
405- for _ in 0 ..100 {
549+ for _ in 0 ..GROWTH_ITERATIONS {
406550 fill_to_capacity ( & allocator) ;
407551
408552 // This check is theoretically redundant because fill_to_capacity
@@ -426,7 +570,7 @@ mod tests {
426570 let reserved_bytes = allocator. reserved_bytes ( ) ;
427571 // The allocations can theoretically be over-allocated, so use >= to
428572 // do the comparison.
429- assert ! ( reserved_bytes >= page_size * 100 ) ;
573+ assert ! ( reserved_bytes >= page_size * GROWTH_ITERATIONS ) ;
430574
431575 // Everything is filled to capacity except the last iteration.
432576 let used_bytes = allocator. used_bytes ( ) ;
0 commit comments