Skip to content

Freed arena memory stays resident in an idle process; no targeted or size-threshold eager purge short of process-wide purge_delay=0 #1422

Closed

Description

@psiha

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.c L2312). It is decommitted after purge_delay × arena_purge_mult (1000 ms × 4), and only when a later collect runs mi_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=0 or arena_purge_mult=0 decommits 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_purge is static, and mi_collect_reduce targets thread-owned memory only.

Request (any one would solve it):

  1. 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.
  2. A targeted release API, e.g. mi_free_and_purge(p) or a flag on mi_free_size, that purges exactly the slices the free returns.
  3. A cheap mi_arenas_purge_expired() that only processes pending purge ranges, without the page/theap collection that mi_collect does. 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

  1. daanx commented on Oct 6, 2026

    @daanx
    Collaborator

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions