@@ -40,6 +40,8 @@ pub struct Dlmalloc<A> {
4040 trim_check : usize ,
4141 least_addr : * mut u8 ,
4242 release_checks : usize ,
43+ granularity : usize ,
44+ max_release_check_rate : usize ,
4345 system_allocator : A ,
4446}
4547unsafe impl < A : Send > Send for Dlmalloc < A > { }
@@ -53,10 +55,13 @@ const TREEBIN_SHIFT: usize = 8;
5355const NSMALLBINS_U32 : u32 = NSMALLBINS as u32 ;
5456const NTREEBINS_U32 : u32 = NTREEBINS as u32 ;
5557
56- // TODO: runtime configurable? documentation?
57- const DEFAULT_GRANULARITY : usize = 64 * 1024 ;
5858const DEFAULT_TRIM_THRESHOLD : usize = 2 * 1024 * 1024 ;
59- const MAX_RELEASE_CHECK_RATE : usize = 4095 ;
59+
60+ // Minimum legal granularity. Smaller values would let `sys_trim` compute
61+ // a non-`malloc_alignment`-aligned residual `topsize`, which corrupts the
62+ // flag bits packed into the top chunk's size encoding. Kept equal to the
63+ // runtime `malloc_alignment()` so chunk math stays sound.
64+ const MIN_GRANULARITY : usize = 2 * mem:: size_of :: < usize > ( ) ;
6065
6166#[ repr( C ) ]
6267struct Chunk {
@@ -127,6 +132,8 @@ impl<A> Dlmalloc<A> {
127132 trim_check : 0 ,
128133 least_addr : ptr:: null_mut ( ) ,
129134 release_checks : 0 ,
135+ granularity : 64 * 1024 ,
136+ max_release_check_rate : 4095 ,
130137 system_allocator,
131138 }
132139 }
@@ -138,6 +145,55 @@ impl<A> Dlmalloc<A> {
138145 pub fn allocator_mut ( & mut self ) -> & mut A {
139146 & mut self . system_allocator
140147 }
148+
149+ /// Sets the maximum number of large-chunk frees between
150+ /// release-unused-segment passes. A value of `0` disables the periodic
151+ /// release check entirely.
152+ ///
153+ /// The new rate is applied immediately: the active countdown is reseeded
154+ /// from the new value (or to `usize::MAX` when disabling). This ensures a
155+ /// disabled -> enabled transition takes effect on the next free rather
156+ /// than after `usize::MAX` decrements.
157+ pub const fn set_max_release_check_rate ( & mut self , rate : usize ) {
158+ self . max_release_check_rate = rate;
159+ self . release_checks = self . release_check_target ( ) ;
160+ }
161+
162+ /// Sets the granularity used for system allocations.
163+ ///
164+ /// Returns `true` if the value was accepted, `false` otherwise. To be
165+ /// accepted, `granularity` must be a power of two and at least
166+ /// `2 * size_of::<usize>()` (the malloc alignment); smaller values are
167+ /// rejected because they would corrupt the flag bits packed into the
168+ /// top chunk's size encoding during `trim`.
169+ ///
170+ /// Unlike C dlmalloc's `mallopt(M_GRANULARITY, ...)`, which rejects
171+ /// sub-page values, this accepts any pow-of-two >= the malloc alignment.
172+ /// Sub-page granularity is intentionally allowed for embedded targets
173+ /// that need tightly-packed allocations on small heaps; the underlying
174+ /// system allocator may still round individual requests up to its page
175+ /// size.
176+ ///
177+ /// For best results call this before the first allocation; existing
178+ /// segments retain their original alignment.
179+ pub const fn set_granularity ( & mut self , granularity : usize ) -> bool {
180+ if !granularity. is_power_of_two ( ) || granularity < MIN_GRANULARITY {
181+ return false ;
182+ }
183+ self . granularity = granularity;
184+ true
185+ }
186+
187+ /// Returns the value to seed `release_checks` with. When the configured
188+ /// rate is zero the periodic release pass is disabled by using
189+ /// `usize::MAX` so the countdown never reaches zero.
190+ const fn release_check_target ( & self ) -> usize {
191+ if self . max_release_check_rate == 0 {
192+ usize:: MAX
193+ } else {
194+ self . max_release_check_rate
195+ }
196+ }
141197}
142198
143199impl < A : Allocator > Dlmalloc < A > {
@@ -187,11 +243,11 @@ impl<A: Allocator> Dlmalloc<A> {
187243 // `max_request` will not be honored
188244 // + self.top_foot_size()
189245 // + self.malloc_alignment()
190- // + DEFAULT_GRANULARITY
246+ // + self.granularity
191247 // ==
192248 // usize::MAX
193249 let min_sys_alloc_space =
194- ( ( !0 - ( DEFAULT_GRANULARITY + self . top_foot_size ( ) + self . malloc_alignment ( ) ) + 1 )
250+ ( ( !0 - ( self . granularity + self . top_foot_size ( ) + self . malloc_alignment ( ) ) + 1 )
195251 & !self . malloc_alignment ( ) )
196252 - self . chunk_overhead ( )
197253 + 1 ;
@@ -386,7 +442,7 @@ impl<A: Allocator> Dlmalloc<A> {
386442 // keep in sync with max_request
387443 let asize = align_up (
388444 size + self . top_foot_size ( ) + self . malloc_alignment ( ) ,
389- DEFAULT_GRANULARITY ,
445+ self . granularity ,
390446 ) ;
391447
392448 let ( tbase, tsize, flags) = self . system_allocator . alloc ( asize) ;
@@ -404,7 +460,7 @@ impl<A: Allocator> Dlmalloc<A> {
404460 self . seg . base = tbase;
405461 self . seg . size = tsize;
406462 self . seg . flags = flags;
407- self . release_checks = MAX_RELEASE_CHECK_RATE ;
463+ self . release_checks = self . release_check_target ( ) ;
408464 self . init_bins ( ) ;
409465 let tsize = tsize - self . top_foot_size ( ) ;
410466 self . init_top ( tbase. cast ( ) , tsize) ;
@@ -562,7 +618,7 @@ impl<A: Allocator> Dlmalloc<A> {
562618 }
563619
564620 // Keep the old chunk if it's big enough but not too big
565- if oldsize >= nb + mem:: size_of :: < usize > ( ) && ( oldsize - nb) <= ( DEFAULT_GRANULARITY << 1 ) {
621+ if oldsize >= nb + mem:: size_of :: < usize > ( ) && ( oldsize - nb) <= ( self . granularity << 1 ) {
566622 return oldp;
567623 }
568624
@@ -1297,7 +1353,7 @@ impl<A: Allocator> Dlmalloc<A> {
12971353 if pad < self . max_request ( ) && !self . top . is_null ( ) {
12981354 pad += self . top_foot_size ( ) ;
12991355 if self . topsize > pad {
1300- let unit = DEFAULT_GRANULARITY ;
1356+ let unit = self . granularity ;
13011357 let extra = ( ( self . topsize - pad + unit - 1 ) / unit - 1 ) * unit;
13021358 let sp = self . segment_holding ( self . top . cast ( ) ) ;
13031359 debug_assert ! ( !sp. is_null( ) ) ;
@@ -1390,11 +1446,7 @@ impl<A: Allocator> Dlmalloc<A> {
13901446 pred = sp;
13911447 sp = next;
13921448 }
1393- self . release_checks = if nsegs > MAX_RELEASE_CHECK_RATE {
1394- nsegs
1395- } else {
1396- MAX_RELEASE_CHECK_RATE
1397- } ;
1449+ self . release_checks = cmp:: max ( nsegs, self . release_check_target ( ) ) ;
13981450 return released;
13991451 }
14001452
@@ -1894,4 +1946,172 @@ mod tests {
18941946 assert_eq ! ( a. malloc( max_request_size) , ptr:: null_mut( ) ) ;
18951947 }
18961948 }
1949+
1950+ #[ test]
1951+ fn default_constructor_preserves_legacy_defaults ( ) {
1952+ // Regression guard: `Dlmalloc::new` must keep the pre-config values so
1953+ // existing wasm32/Linux targets see no behavioral change.
1954+ let a = Dlmalloc :: new ( System :: new ( ) ) ;
1955+ assert_eq ! ( a. granularity, 64 * 1024 ) ;
1956+ assert_eq ! ( a. max_release_check_rate, 4095 ) ;
1957+ }
1958+
1959+ // Verifies the const-fn setter chain works end-to-end in a `const` block
1960+ // (the supported pattern in lieu of dedicated constructors). Validation
1961+ // failures become compile-time errors via `assert!`.
1962+ #[ test]
1963+ fn const_block_configuration ( ) {
1964+ let a = const {
1965+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
1966+ assert ! ( a. set_granularity( MIN_GRANULARITY * 2 ) ) ;
1967+ a. set_max_release_check_rate ( 0 ) ;
1968+ a
1969+ } ;
1970+ assert_eq ! ( a. granularity, MIN_GRANULARITY * 2 ) ;
1971+ assert_eq ! ( a. max_release_check_rate, 0 ) ;
1972+ assert_eq ! ( a. release_checks, usize :: MAX ) ;
1973+ }
1974+
1975+ #[ test]
1976+ fn set_granularity_validates ( ) {
1977+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
1978+
1979+ // non-power-of-two rejected
1980+ assert ! ( !a. set_granularity( 3 * 1024 ) ) ;
1981+ assert_eq ! ( a. granularity, 64 * 1024 ) ;
1982+
1983+ // zero rejected (is_power_of_two(0) == false)
1984+ assert ! ( !a. set_granularity( 0 ) ) ;
1985+ assert_eq ! ( a. granularity, 64 * 1024 ) ;
1986+
1987+ // below malloc_alignment rejected
1988+ assert ! ( !a. set_granularity( MIN_GRANULARITY / 2 ) ) ;
1989+ assert_eq ! ( a. granularity, 64 * 1024 ) ;
1990+
1991+ // exactly malloc_alignment accepted (the smallest legal value)
1992+ assert ! ( a. set_granularity( MIN_GRANULARITY ) ) ;
1993+ assert_eq ! ( a. granularity, MIN_GRANULARITY ) ;
1994+
1995+ // sub-page but >= malloc_alignment accepted (the embedded use case)
1996+ let page = a. system_allocator . page_size ( ) ;
1997+ let sub_page = MIN_GRANULARITY * 2 ;
1998+ assert ! ( sub_page < page) ;
1999+ assert ! ( a. set_granularity( sub_page) ) ;
2000+ assert_eq ! ( a. granularity, sub_page) ;
2001+
2002+ // page-sized pow2 accepted
2003+ assert ! ( a. set_granularity( page) ) ;
2004+ assert_eq ! ( a. granularity, page) ;
2005+
2006+ // larger pow2 accepted
2007+ assert ! ( a. set_granularity( 2 * 64 * 1024 ) ) ;
2008+ assert_eq ! ( a. granularity, 2 * 64 * 1024 ) ;
2009+ }
2010+
2011+ #[ test]
2012+ fn set_max_release_check_rate_zero_disables_countdown ( ) {
2013+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
2014+ a. set_max_release_check_rate ( 0 ) ;
2015+ assert_eq ! ( a. max_release_check_rate, 0 ) ;
2016+ assert_eq ! ( a. release_checks, usize :: MAX ) ;
2017+ }
2018+
2019+ // Regression test for the disabled -> enabled bug: prior to the fix, the
2020+ // setter only reseeded the countdown when transitioning to `0`, so a
2021+ // subsequent positive rate left `release_checks` at `usize::MAX` and the
2022+ // periodic pass effectively never fired again.
2023+ #[ test]
2024+ fn set_max_release_check_rate_reenable_reseeds_countdown ( ) {
2025+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
2026+ a. set_max_release_check_rate ( 0 ) ;
2027+ assert_eq ! ( a. release_checks, usize :: MAX ) ;
2028+
2029+ a. set_max_release_check_rate ( 8 ) ;
2030+ assert_eq ! ( a. max_release_check_rate, 8 ) ;
2031+ assert_eq ! ( a. release_checks, 8 ) ;
2032+ }
2033+
2034+ #[ test]
2035+ fn set_max_release_check_rate_updates_active_countdown ( ) {
2036+ // The setter takes effect immediately even when going between two
2037+ // non-zero rates; this avoids a stale large countdown sticking around
2038+ // after the user lowers the rate.
2039+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
2040+ assert_eq ! ( a. max_release_check_rate, 4095 ) ;
2041+ a. set_max_release_check_rate ( 2 ) ;
2042+ assert_eq ! ( a. max_release_check_rate, 2 ) ;
2043+ assert_eq ! ( a. release_checks, 2 ) ;
2044+ }
2045+
2046+ // End-to-end: drive enough large-chunk frees under a tiny release rate to
2047+ // tick the countdown to zero and trigger `release_unused_segments` from
2048+ // `dispose_chunk`. The test passes if the allocator stays consistent.
2049+ #[ test]
2050+ #[ cfg( not( miri) ) ]
2051+ fn small_release_rate_drives_periodic_pass ( ) {
2052+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
2053+ a. set_max_release_check_rate ( 2 ) ;
2054+ let large = NSMALLBINS * ( 1 << SMALLBIN_SHIFT ) ;
2055+ assert ! ( !a. is_small( large) ) ;
2056+ unsafe {
2057+ for _ in 0 ..32 {
2058+ let p1 = a. malloc ( large) ;
2059+ let p2 = a. malloc ( large) ;
2060+ assert ! ( !p1. is_null( ) ) ;
2061+ assert ! ( !p2. is_null( ) ) ;
2062+ a. free ( p1) ;
2063+ a. free ( p2) ;
2064+ }
2065+ }
2066+ }
2067+
2068+ // End-to-end: page-sized custom granularity survives a basic workload.
2069+ #[ test]
2070+ #[ cfg( not( miri) ) ]
2071+ fn custom_page_granularity_alloc_free ( ) {
2072+ let page = System :: new ( ) . page_size ( ) ;
2073+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
2074+ assert ! ( a. set_granularity( page) ) ;
2075+ assert_eq ! ( a. granularity, page) ;
2076+ unsafe {
2077+ let mut ptrs = [ ptr:: null_mut :: < u8 > ( ) ; 16 ] ;
2078+ for ( i, slot) in ptrs. iter_mut ( ) . enumerate ( ) {
2079+ let p = a. malloc ( 64 + i * 7 ) ;
2080+ assert ! ( !p. is_null( ) ) ;
2081+ * p = i as u8 ;
2082+ * slot = p;
2083+ }
2084+ for ( i, & p) in ptrs. iter ( ) . enumerate ( ) {
2085+ assert_eq ! ( * p, i as u8 ) ;
2086+ a. free ( p) ;
2087+ }
2088+ }
2089+ }
2090+
2091+ // End-to-end: sub-page granularity (the embedded-target use case the
2092+ // configurable granularity was added for). The system allocator may
2093+ // still round individual requests up to its page size, but dlmalloc
2094+ // itself must remain consistent.
2095+ #[ test]
2096+ #[ cfg( not( miri) ) ]
2097+ fn custom_sub_page_granularity_alloc_free ( ) {
2098+ let sub_page = MIN_GRANULARITY * 2 ;
2099+ assert ! ( sub_page < System :: new( ) . page_size( ) ) ;
2100+ let mut a = Dlmalloc :: new ( System :: new ( ) ) ;
2101+ assert ! ( a. set_granularity( sub_page) ) ;
2102+ assert_eq ! ( a. granularity, sub_page) ;
2103+ unsafe {
2104+ let mut ptrs = [ ptr:: null_mut :: < u8 > ( ) ; 16 ] ;
2105+ for ( i, slot) in ptrs. iter_mut ( ) . enumerate ( ) {
2106+ let p = a. malloc ( 64 + i * 7 ) ;
2107+ assert ! ( !p. is_null( ) ) ;
2108+ * p = i as u8 ;
2109+ * slot = p;
2110+ }
2111+ for ( i, & p) in ptrs. iter ( ) . enumerate ( ) {
2112+ assert_eq ! ( * p, i as u8 ) ;
2113+ a. free ( p) ;
2114+ }
2115+ }
2116+ }
18972117}
0 commit comments