Skip to content

Commit 30f026c

Browse files
authored
Merge pull request #68 from arthurkomatsu/feat/configurable-granularity
Make granularity and release-check rate configurable
2 parents 1ef304b + 107b3dc commit 30f026c

3 files changed

Lines changed: 351 additions & 14 deletions

File tree

src/dlmalloc.rs

Lines changed: 234 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
4547
unsafe impl<A: Send> Send for Dlmalloc<A> {}
@@ -53,10 +55,13 @@ const TREEBIN_SHIFT: usize = 8;
5355
const NSMALLBINS_U32: u32 = NSMALLBINS as u32;
5456
const NTREEBINS_U32: u32 = NTREEBINS as u32;
5557

56-
// TODO: runtime configurable? documentation?
57-
const DEFAULT_GRANULARITY: usize = 64 * 1024;
5858
const 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)]
6267
struct 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

143199
impl<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
}

src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,54 @@ impl<A> Dlmalloc<A> {
104104
pub const fn new_with_allocator(sys_allocator: A) -> Dlmalloc<A> {
105105
Dlmalloc(dlmalloc::Dlmalloc::new(sys_allocator))
106106
}
107+
108+
/// Sets the maximum number of large-chunk frees between periodic
109+
/// release-unused-segments passes. A value of `0` disables the pass.
110+
///
111+
/// May be called at any time. The new rate takes effect immediately: the
112+
/// active countdown is reseeded from the new value, so a disabled ->
113+
/// enabled transition fires on the next free rather than after
114+
/// `usize::MAX` decrements.
115+
pub const fn set_max_release_check_rate(&mut self, rate: usize) {
116+
self.0.set_max_release_check_rate(rate);
117+
}
118+
119+
/// Sets the granularity used for system allocations.
120+
///
121+
/// Returns `true` if the value was accepted, `false` otherwise. To be
122+
/// accepted, `granularity` must be a power of two and at least
123+
/// `2 * size_of::<usize>()` (the malloc alignment); smaller values are
124+
/// rejected because they would break the chunk size/flag-bit invariants
125+
/// during `trim`.
126+
///
127+
/// Unlike C dlmalloc's `mallopt(M_GRANULARITY, ...)`, which rejects
128+
/// sub-page values, this accepts any pow-of-two >= the malloc alignment.
129+
/// Sub-page granularity is intentionally allowed for embedded targets
130+
/// that need tightly-packed allocations.
131+
///
132+
/// For best results call this before the first allocation; existing
133+
/// segments retain their original alignment.
134+
///
135+
/// # Const context
136+
///
137+
/// This is a `const fn`, so a configured allocator can be built in a
138+
/// `const` block:
139+
///
140+
/// ```ignore
141+
/// let mut alloc = const {
142+
/// let mut a = Dlmalloc::new();
143+
/// assert!(a.set_granularity(32 * core::mem::size_of::<usize>()));
144+
/// a.set_max_release_check_rate(0);
145+
/// a
146+
/// };
147+
/// ```
148+
///
149+
/// `Dlmalloc` is `Send` but not `Sync`, so it cannot be placed directly
150+
/// in a `static`; use `GlobalDlmalloc` (behind the `global` feature)
151+
/// when a `static` allocator is needed.
152+
pub const fn set_granularity(&mut self, granularity: usize) -> bool {
153+
self.0.set_granularity(granularity)
154+
}
107155
}
108156

109157
impl<A: Allocator> Dlmalloc<A> {

0 commit comments

Comments
 (0)