From 9ba853271a947a3512e052481dcb6c0ac538578b Mon Sep 17 00:00:00 2001 From: Maik Schulze Date: Sat, 7 Nov 2026 06:16:04 +0100 Subject: [PATCH 1/2] gh-161420: Process mimalloc's delayed frees on every generic malloc again Since the update to mimalloc 2.5.2 (gh-159111), _mi_malloc_generic() processes the heap's delayed frees only every 100 calls. A block over MI_MEDIUM_OBJ_SIZE_MAX has a page of its own, so a free of it from another thread is a delayed free, and every allocation of such a block is a generic malloc: each thread could keep up to about 100 large blocks that other threads had freed. Process the delayed frees on every generic malloc again, as mimalloc 2.1 did; the deferred-free callback and the periodic collection stay batched. --- .../2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst | 6 ++++++ Objects/mimalloc/page.c | 10 +++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst new file mode 100644 index 000000000000000..c79d85f2f3691ea --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst @@ -0,0 +1,6 @@ +Fix the :term:`free-threaded build`, and builds with :envvar:`PYTHONMALLOC` +set to ``mimalloc``, holding on to memory that other threads freed: since the +update to mimalloc 2.5.2, each thread could keep up to 100 large blocks (over +64 KiB) that other threads had freed, so a thread pool producing large objects +for another thread could reach peaks of several gigabytes. Such blocks are +reused right away again. diff --git a/Objects/mimalloc/page.c b/Objects/mimalloc/page.c index 4bce8ce76044cbe..5d34749a37204e4 100644 --- a/Objects/mimalloc/page.c +++ b/Objects/mimalloc/page.c @@ -1071,6 +1071,13 @@ void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_al } mi_assert_internal(mi_heap_is_initialized(heap)); + // free delayed frees from other threads (but skip contended ones) + // gh-161420: on every generic malloc, as in mimalloc 2.1, not every 100th. A block over + // MI_MEDIUM_OBJ_SIZE_MAX has a page of its own, so a free of it from another thread is a + // delayed free; processed every 100th generic malloc only, a heap could keep up to about 100 + // such blocks from being reused. + _mi_heap_delayed_free_partial(heap); + // do administrative tasks every N generic mallocs if mi_unlikely(++heap->generic_count >= 100) { heap->generic_collect_count += heap->generic_count; @@ -1078,9 +1085,6 @@ void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_al // call potential deferred free routines _mi_deferred_free(heap, false); - // free delayed frees from other threads (but skip contended ones) - _mi_heap_delayed_free_partial(heap); - // collect every once in a while (10000 by default) const long generic_collect = mi_option_get_clamp(mi_option_generic_collect, 1, 1000000L); if (heap->generic_collect_count >= generic_collect) { From b4fe0c0bfc86039175e19584c8c15386b2fc54d7 Mon Sep 17 00:00:00 2001 From: Maik Schulze Date: Sat, 7 Nov 2026 07:41:35 +0100 Subject: [PATCH 2/2] Apply batched suggestions from code review Co-authored-by: Donghee Na --- .../2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst | 8 ++------ Objects/mimalloc/page.c | 9 ++++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst index c79d85f2f3691ea..296a2267f397be0 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-07-09-30-00.gh-issue-161420.mIdFre.rst @@ -1,6 +1,2 @@ -Fix the :term:`free-threaded build`, and builds with :envvar:`PYTHONMALLOC` -set to ``mimalloc``, holding on to memory that other threads freed: since the -update to mimalloc 2.5.2, each thread could keep up to 100 large blocks (over -64 KiB) that other threads had freed, so a thread pool producing large objects -for another thread could reach peaks of several gigabytes. Such blocks are -reused right away again. +Restore ``mimalloc``'s previous behavior of processing delayed frees from other +threads on every generic malloc call, reverting the change introduced in v2.2.2. diff --git a/Objects/mimalloc/page.c b/Objects/mimalloc/page.c index 5d34749a37204e4..2f26d04aaaa4d87 100644 --- a/Objects/mimalloc/page.c +++ b/Objects/mimalloc/page.c @@ -1071,11 +1071,10 @@ void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_al } mi_assert_internal(mi_heap_is_initialized(heap)); - // free delayed frees from other threads (but skip contended ones) - // gh-161420: on every generic malloc, as in mimalloc 2.1, not every 100th. A block over - // MI_MEDIUM_OBJ_SIZE_MAX has a page of its own, so a free of it from another thread is a - // delayed free; processed every 100th generic malloc only, a heap could keep up to about 100 - // such blocks from being reused. + // Process delayed frees from other threads (but skip contended ones). + // CPython: gh-161420 showed that calling _mi_heap_delayed_free_partial only + // once every 100 generic malloc calls led to high memory usage. + // To avoid this, we now process delayed frees on every generic malloc call again. _mi_heap_delayed_free_partial(heap); // do administrative tasks every N generic mallocs