I see a large memory-footprint regression starting between mimalloc 3.1.6 and 3.2.6 when objects are allocated by multiple threads but freed by a dedicated thread which has never allocated memory or called mi_thread_init().
The attached mimalloc_uninitialized_remote_free_repro.c, written for me by ChatGPT, reproduces this. It creates 16 producer threads, each maintaining a fixed live set of 592-byte allocations. Replaced objects are passed through bounded queues to one reclaimer thread which only calls free(). Thus the logical outstanding allocation size is strictly bounded at about 370 MiB.
Build, for example:
gcc -O2 -pthread mimalloc_uninitialized_remote_free_repro.c
-ldl -o mimalloc-repro
Run against a particular mimalloc shared library:
LD_PRELOAD=/path/to/libmimalloc.so \
MIMALLOC_SHOW_STATS=1 \
./mimalloc-repro 30
The optional --init-reclaimer makes the reclaimer thread call mi_thread_init() before doing any frees:
LD_PRELOAD=/path/to/libmimalloc.so \
MIMALLOC_SHOW_STATS=1 \
./mimalloc-repro 30 --init-reclaimer
Results on my system (x86-64 Linux, Ubuntu 26.04):
steady RSS
3.1.6, reclaimer not initialized ~437 MiB
3.2.6, reclaimer not initialized ~1.80 GiB
3.4.5, reclaimer not initialized ~1.77 GiB
3.4.5, reclaimer initialized ~441 MiB
In all cases the application allocation/free counts remain bounded and end with exactly zero outstanding allocations.
With 3.4.5, initializing only the reclaimer also changes the allocator statistics dramatically:
without mi_thread_init: reclaima=0, reabandon=0
with mi_thread_init: reclaima=62.0M, reabandon=62.0M
My reading of the v3 code is that remote frees of partially-used abandoned pages cannot successfully reclaim/reabandon the page when _mi_page_associated_theap_peek() finds no theap for the freeing thread. The page therefore cannot be efficiently reused until its remaining allocations have all been freed.
Since free() is expected to work from a thread which has never allocated, requiring an explicit mi_thread_init() on such a thread is unfortunate. Is this intended, or should the remote-free path handle this case without application intervention?
mimalloc_uninitialized_remote_free_repro.c
I see a large memory-footprint regression starting between mimalloc 3.1.6 and 3.2.6 when objects are allocated by multiple threads but freed by a dedicated thread which has never allocated memory or called mi_thread_init().
The attached mimalloc_uninitialized_remote_free_repro.c, written for me by ChatGPT, reproduces this. It creates 16 producer threads, each maintaining a fixed live set of 592-byte allocations. Replaced objects are passed through bounded queues to one reclaimer thread which only calls free(). Thus the logical outstanding allocation size is strictly bounded at about 370 MiB.
Build, for example:
gcc -O2 -pthread mimalloc_uninitialized_remote_free_repro.c
-ldl -o mimalloc-repro
Run against a particular mimalloc shared library:
The optional --init-reclaimer makes the reclaimer thread call mi_thread_init() before doing any frees:
Results on my system (x86-64 Linux, Ubuntu 26.04):
In all cases the application allocation/free counts remain bounded and end with exactly zero outstanding allocations.
With 3.4.5, initializing only the reclaimer also changes the allocator statistics dramatically:
My reading of the v3 code is that remote frees of partially-used abandoned pages cannot successfully reclaim/reabandon the page when _mi_page_associated_theap_peek() finds no theap for the freeing thread. The page therefore cannot be efficiently reused until its remaining allocations have all been freed.
Since free() is expected to work from a thread which has never allocated, requiring an explicit mi_thread_init() on such a thread is unfortunate. Is this intended, or should the remote-free path handle this case without application intervention?
mimalloc_uninitialized_remote_free_repro.c