Repository navigation
Description
Version: v3.5.4 (f8401bef, dev3); same code on the default branch main3. Linux 7.0 x86-64, gcc -O2, default options (THP madvise).
Problem: a freed arena-backed block is never returned to the OS while the process stays idle, and the only immediate release is process-wide.
Workload: a long-running service that allocates and frees blocks of tens to hundreds of MiB (large containers that grow, then get dropped). It needs RSS to fall soon after such a free, because it runs under a memory limit and then sits idle between requests.
Measured with the repro below (output verbatim). Each block is allocated, touched and freed on one thread with nothing else running; RSS in MiB:
16 MiB: after free 20, +5 s idle 20, after mi_collect(false) 6 MiB
64 MiB: after free 72, +5 s idle 72, after mi_collect(false) 6 MiB
192 MiB: after free 200, +5 s idle 200, after mi_collect(false) 6 MiB
256 MiB: after free 6, +5 s idle 6, after mi_collect(false) 6 MiB
- In this fresh process (default
arena_reserve, 1 GiB), blocks up to 192 MiB come from an arena. - On free, the range is only scheduled (
mi_arena_schedule_purge,src/arena.cL2312). It is decommitted afterpurge_delay×arena_purge_mult(1000 ms × 4), and only when a later collect runsmi_arenas_try_purge. A process that goes idle never collects, so the memory stays resident indefinitely. - The 256 MiB block was OS-mapped here and was released at once. That depends on arena reservation sizing, though: arenas take objects up to
arena_max_object_size(2 GiB). Larger blocks served from an existing arena behave the same way. - So whether a freed block's RSS comes back depends on allocator-internal sizing and on whether the process happens to allocate again.
Workarounds today, all process-wide:
purge_delay=0orarena_purge_mult=0decommits every arena free at once, small ones included. Those get recommitted and faulted in again on reuse, which is constant churn.mi_collect(true)/mi_heap_collect(h, true)bypasses the delay, but it is a full collection: every pending range in every arena of the subprocess, plus the calling thread's pages. That is too heavy to call after each big free.mi_arena_purgeis static, andmi_collect_reducetargets thread-owned memory only.
Request (any one would solve it):
- A size-threshold option (e.g.
mi_option_purge_eager_size): freed arena ranges at or above it are purged on free, ignoring the delay. Smaller frees keep today's delayed behaviour. - A targeted release API, e.g.
mi_free_and_purge(p)or a flag onmi_free_size, that purges exactly the slices the free returns. - A cheap
mi_arenas_purge_expired()that only processes pending purge ranges, without the page/theap collection thatmi_collectdoes. An idle process could then call it from a timer.
Repro:
#include <mimalloc.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static double rss_mib(void) { long a, r; FILE* f = fopen("/proc/self/statm", "r"); fscanf(f, "%ld %ld", &a, &r); fclose(f); return r * (double)sysconf(_SC_PAGESIZE) / 1048576.0; }
int main(void) {
size_t sizes[] = { 16, 64, 192, 256 };
for (int i = 0; i < 4; i++) {
size_t n = sizes[i] << 20;
void* p = mi_malloc(n); memset(p, 1, n); mi_free(p);
printf("%zu MiB: after free %.0f", sizes[i], rss_mib());
sleep(5); printf(", +5 s idle %.0f", rss_mib());
mi_collect(false); printf(", after mi_collect(false) %.0f MiB\n", rss_mib());
}
}Build: gcc -O2 -I include src/static.c repro.c -lpthread.
Related: #1105, #1187, #1392 (commit tracking and limits for pods), #416 and #536 (cost of mi_collect).
Activity
add mi_option_arena_purge_immediate_size and explicit mi_arenas_purge…
daanx commentedon Oct 6, 2026
Thanks! I added 2 of the suggestions: any "huge" object (> large page size = 4 MiB) is now immediately purged and also i added a function
mi_arenas_purge()for purging outstanding ranges manually.