[RFC PATCH 0/1] malloc: madvise interior free chunks above a threshold
Xavier Roche
xavier.roche@algolia.com
Tue Apr 21 08:39:11 GMT 2026
Since glibc 2.26 introduced tcache, free() no longer returns
physical pages to the kernel for chunks that sit in the interior
of an arena heap. Periodic malloc_trim(0) recovers the memory,
but free() itself cannot. A bisect across 2.23-2.27 with the
reproducer (attached as malloc/tst-madvise-threshold.c) shows
a 4x RSS jump between 2.25 and 2.26.
BZ #33886 is the latest thread on this. BZ #15321, #18910,
#27976 describe older aspects of the same behavior. Several
projects ship workarounds: systemd, OpenJDK, Python, and our
own production search engine, which overrides free() and
triggers malloc_trim(0) on a background thread.
Mechanism
---------
_int_free_maybe_trim already runs for consolidated chunks
>= 64 KB and calls systrim / heap_trim on the top chunk. This
patch extends it to madvise the page-aligned interior of the
consolidated chunk (same page-alignment logic as mtrim()).
An early version triggered madvise on every qualifying free,
which Wilco Dijkstra pointed out could produce ~1M syscalls
when a million small frees merge into one chunk (BZ #33886
comment 10). To address that, the patch uses the caller's
original (pre-merge) chunk size as the gate:
- orig_size >= one page + chunk header: madvise immediately.
- orig_size smaller: accumulate in a per-arena counter, and
fire madvise only when the accumulator crosses 256 KB.
In the worst case (one million 100-byte frees merging into a
100 MB chunk), this produces ~400 madvise calls instead of ~1M.
Normal workloads pay one accumulator increment per small free
and one madvise per 256 KB of pressure.
The per-arena counter is accessed only under av->mutex
(verified at both call sites: _int_free_merge_chunk and
_int_memalign).
For advice type: MADV_FREE for moderate chunks (lazy release,
no re-fault on quick reuse); MADV_DONTNEED for chunks >= 128 KB
(immediate RSS reduction matches what operators expect from
mtrim). This split is an open design point; simplifying to
MADV_DONTNEED unconditionally is an option if preferred.
Impact
------
Reproducer: 16 threads, 256 MB live working set, 10 GB of
short-lived allocations below mmap threshold.
baseline patched
RSS after free 1247 MB 296 MB
malloc_trim recovery 962 MB 14 MB (free did the work)
Runtime 0.34 s 0.50 s (tight malloc loop)
Runtime overhead is only visible in synthetic malloc-heavy
benchmarks. Real applications spend a fraction of a percent
of their time in free(); the amortised cost is negligible.
Algolia production (the motivating workload) recovers 100+ GB
of previously-wasted RSS with equivalent semantics.
Background: https://www.algolia.com/blog/engineering/when-allocators-are-hoarding-your-precious-memory
Testing
-------
- malloc test suite: all PASS, including the new
tst-madvise-threshold that validates the RSS drop.
- Full make check on x86_64-linux-gnu: no new regressions
vs baseline.
- Cross-built via build-many-glibcs.py for
aarch64-linux-gnu, arm-linux-gnueabihf,
powerpc64le-linux-gnu, s390x-linux-gnu: check-compilers,
build and compile-only check passed on all four with no
new warnings on malloc.c. (i686 is covered by patchwork.)
Open questions
--------------
1. MADV_FREE vs MADV_DONTNEED split at 128 KB: keep the two-tier
design, or always use MADV_DONTNEED (matches mtrim())?
2. Default-on vs tunable: the current patch is default-on on the
argument that this is a regression fix since 2.26. A tunable
kill-switch (glibc.malloc.madvise_interior, default on) could
be added if preferred.
3. Threshold values (64 KB entry gate, 256 KB accumulator, 128 KB
MADV_DONTNEED boundary) are picked as multiples of
ATTEMPT_TRIMMING_THRESHOLD; open to tuning.
Acknowledgments
---------------
Thanks to Wilco Dijkstra for the earlier review on BZ #33886
that surfaced the million-madvise-calls concern and redirected
the design toward the accumulator approach. The bisect and
tcache-as-root-cause framing also came out of that discussion.
Related prior art: Mel Gorman's 2015 RFC
(https://sourceware.org/legacy-ml/libc-alpha/2015-02/msg00193.html)
explored a related madvise refault-avoidance question; this
patch is narrower in scope.
AI disclosure
-------------
AI assistance (Claude) was used during analysis and drafting:
reading the ptmalloc2 source, reviewing the regression bisect,
exploring design alternatives, and iterating on the patch text.
The design decisions, the benchmark runs, and the final code
were reviewed and validated by the author, who takes
responsibility for correctness.
Xavier Roche (1):
malloc: madvise interior free chunks above a threshold
malloc/Makefile | 1 +
malloc/malloc.c | 61 +++++++++++++---
malloc/tst-madvise-threshold.c | 128 +++++++++++++++++++++++++++++++++
3 files changed, 182 insertions(+), 8 deletions(-)
create mode 100644 malloc/tst-madvise-threshold.c
base-commit: e3e8f814e53d32da99db03ac38ed3af86651b233
--
2.43.0
More information about the Libc-alpha
mailing list