Skip to content

Commit fa3a143

Browse files
committed
Re-apply CPython-specific changes to mimalloc v2.5.2
Most of the previous downstream changes were upstreamed in mimalloc v2.1.3 - v2.1.7 (heap tags, _mi_heap_init/_mi_tld_init, MI_PRIM_THREAD_ID, sub-processes, heap walk optimizations, platform fixes) and are dropped. Remaining CPython-specific changes, all marked with a "CPython" comment: - QSBR delayed page freeing for free-threaded builds (gh-115103, gh-145615): mi_page_t/mi_heap_t fields and _PyMem_mi_* hooks in page.c, heap.c and segment.c. - debug_offset: preserve the first bytes of a PyObject (ob_tid, refcount fields) when filling freed or uninitialized memory in debug builds (gh-115491). - Atomic mi_block_t.next loads/stores to avoid TSAN reports (gh-129748). - Relax the owning thread assertion in _mi_free_delayed_block for free-threaded builds, since the GC traverses heaps of other threads. - _mi_abandoned_visit_blocks_unlocked(): visit abandoned segments of a sub-process without locks or mi_option_visit_abandoned. Only safe while the world is stopped. - Call _mi_heap_delayed_free_partial() in mi_heap_visit_blocks() (gh-112532). - Only count reclaimed pages up to MI_MEDIUM_OBJ_SIZE_MAX as a "right page" in mi_segment_reclaim (gh-115103). - Rename the weak _ZSt15get_new_handlerv symbol and disable the mi_is_in_heap_region debug warning. - Zero-initialize _mi_page_empty instead of a positional initializer.
1 parent 4180f6d commit fa3a143

9 files changed

Lines changed: 143 additions & 39 deletions

File tree

‎Include/internal/mimalloc/mimalloc/internal.h‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ void _mi_abandoned_reclaim_all(mi_heap_t* heap, mi_segments_tld_t* tld);
249249
void _mi_abandoned_collect(mi_heap_t* heap, bool force, mi_segments_tld_t* tld);
250250
bool _mi_segment_attempt_reclaim(mi_heap_t* heap, mi_segment_t* segment);
251251
bool _mi_segment_visit_blocks(mi_segment_t* segment, int heap_tag, bool visit_blocks, mi_block_visit_fun* visitor, void* arg);
252+
bool _mi_abandoned_visit_blocks_unlocked(mi_subproc_t* subproc, int heap_tag, bool visit_blocks, mi_block_visit_fun* visitor, void* arg); // CPython
252253

253254
// "page.c"
254255
void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_alignment, mi_page_t** ppage) mi_attr_noexcept mi_attr_malloc;
@@ -846,10 +847,10 @@ static inline mi_block_t* mi_block_nextx( const void* null, const mi_block_t* bl
846847
mi_track_mem_defined(block,sizeof(mi_block_t));
847848
mi_block_t* next;
848849
#ifdef MI_ENCODE_FREELIST
849-
next = (mi_block_t*)mi_ptr_decode(null, block->next, keys);
850+
next = (mi_block_t*)mi_ptr_decode(null, mi_atomic_load_relaxed((_Atomic(mi_encoded_t)*)&block->next), keys);
850851
#else
851852
MI_UNUSED(keys); MI_UNUSED(null);
852-
next = (mi_block_t*)block->next;
853+
next = (mi_block_t*)mi_atomic_load_relaxed((_Atomic(mi_encoded_t)*)&block->next);
853854
#endif
854855
mi_track_mem_noaccess(block,sizeof(mi_block_t));
855856
return next;
@@ -858,10 +859,10 @@ static inline mi_block_t* mi_block_nextx( const void* null, const mi_block_t* bl
858859
static inline void mi_block_set_nextx(const void* null, mi_block_t* block, const mi_block_t* next, const uintptr_t* keys) {
859860
mi_track_mem_undefined(block,sizeof(mi_block_t));
860861
#ifdef MI_ENCODE_FREELIST
861-
block->next = mi_ptr_encode(null, next, keys);
862+
mi_atomic_store_relaxed(&block->next, mi_ptr_encode(null, next, keys));
862863
#else
863864
MI_UNUSED(keys); MI_UNUSED(null);
864-
block->next = (mi_encoded_t)next;
865+
mi_atomic_store_relaxed(&block->next, (mi_encoded_t)next);
865866
#endif
866867
mi_track_mem_noaccess(block,sizeof(mi_block_t));
867868
}

‎Include/internal/mimalloc/mimalloc/types.h‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ typedef size_t mi_threadid_t;
248248

249249
// free lists contain blocks
250250
typedef struct mi_block_s {
251-
mi_encoded_t next;
251+
_Atomic(mi_encoded_t) next; // CPython: atomic to avoid TSAN reports from concurrent heap visits (gh-129748)
252252
} mi_block_t;
253253

254254
#if MI_GUARDED
@@ -341,6 +341,8 @@ typedef struct mi_page_s {
341341
uint16_t used; // number of blocks in use (including blocks in `thread_free`)
342342
uint8_t block_size_shift; // if not zero, then `(1 << block_size_shift) == block_size` (only used for fast path in `free.c:_mi_page_ptr_unalign`)
343343
uint8_t heap_tag; // tag of the owning heap, used to separate heaps by object type
344+
uint8_t use_qsbr:1; // CPython: delay page freeing using QSBR
345+
uint8_t debug_offset; // CPython: number of bytes to preserve when filling freed or uninitialized memory
344346
// padding
345347
size_t block_size; // size available in each block (always `>0`)
346348
uint8_t* page_start; // start of the page area containing the blocks
@@ -355,8 +357,13 @@ typedef struct mi_page_s {
355357
struct mi_page_s* next; // next page owned by this thread with the same `block_size`
356358
struct mi_page_s* prev; // previous page owned by this thread with the same `block_size`
357359

360+
#ifdef Py_GIL_DISABLED
361+
struct llist_node qsbr_node; // CPython: list of pages waiting for a QSBR goal before being freed
362+
uint64_t qsbr_goal; // CPython: QSBR goal to reach before the page can be freed (0 if none)
363+
#else
358364
// 64-bit 11 words, 32-bit 13 words, (+2 for secure)
359365
void* padding[1];
366+
#endif
360367
} mi_page_t;
361368

362369

@@ -573,6 +580,8 @@ struct mi_heap_s {
573580
mi_heap_t* next; // list of heaps per thread
574581
bool no_reclaim; // `true` if this heap should not reclaim abandoned pages
575582
uint8_t tag; // custom tag, can be used for separating heaps based on the object types
583+
uint8_t debug_offset; // CPython: number of bytes to preserve when filling freed or uninitialized memory
584+
bool page_use_qsbr; // CPython: should freeing pages be delayed using QSBR
576585
#if MI_GUARDED
577586
size_t guarded_size_min; // minimal size for guarded objects
578587
size_t guarded_size_max; // maximal size for guarded objects

‎Objects/mimalloc/alloc.c‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ terms of the MIT license. A copy of the license can be found in the file
1616
#include <string.h> // memset, strlen (for mi_strdup)
1717
#include <stdlib.h> // malloc, abort
1818

19+
// CPython: don't export the weak C++ `std::get_new_handler` symbol from libpython
20+
#define _ZSt15get_new_handlerv _Py__ZSt15get_new_handlerv
21+
22+
#if (MI_DEBUG>0)
23+
// CPython: fill freed or uninitialized memory but preserve the first `page->debug_offset`
24+
// bytes (e.g. `ob_tid` and the reference count fields that lock-free readers may still access)
25+
static inline void mi_debug_fill(mi_page_t* page, mi_block_t* block, int c, size_t size) {
26+
size_t offset = (size_t)page->debug_offset;
27+
if (offset < size) {
28+
memset((char*)block + offset, c, size - offset);
29+
}
30+
}
31+
#endif
32+
1933
#define MI_IN_ALLOC_C
2034
#include "alloc-override.c"
2135
#include "free.c"
@@ -74,7 +88,7 @@ extern inline void* _mi_page_malloc_zero(mi_heap_t* heap, mi_page_t* page, size_
7488

7589
#if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN
7690
if (!zero && !mi_page_is_huge(page)) {
77-
memset(block, MI_DEBUG_UNINIT, mi_page_usable_block_size(page));
91+
mi_debug_fill(page, block, MI_DEBUG_UNINIT, mi_page_usable_block_size(page));
7892
}
7993
#elif (MI_SECURE!=0)
8094
if (!zero) { block->next = 0; } // don't leak internal data

‎Objects/mimalloc/arena-abandon.c‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,42 @@ bool mi_abandoned_visit_blocks(mi_subproc_id_t subproc_id, int heap_tag, bool vi
344344
_mi_arena_field_cursor_done(&current);
345345
return ok;
346346
}
347+
348+
349+
/* -----------------------------------------------------------
350+
CPython: visit all abandoned segments of a sub-process without
351+
taking any locks or claiming the segments.
352+
353+
Unlike `mi_abandoned_visit_blocks`, this does not require
354+
`mi_option_visit_abandoned` to be enabled. The caller must
355+
guarantee that no other thread abandons or reclaims segments
356+
concurrently (in CPython: a stop-the-world pause), and the
357+
visitor must not allocate or free mimalloc managed memory.
358+
----------------------------------------------------------- */
359+
360+
bool _mi_abandoned_visit_blocks_unlocked(mi_subproc_t* subproc, int heap_tag, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) {
361+
// segments in arenas are tracked in the `blocks_abandoned` bitmaps
362+
const size_t max_arena = mi_arena_get_count();
363+
for (size_t arena_idx = 0; arena_idx < max_arena; arena_idx++) {
364+
mi_arena_t* arena = mi_arena_from_index(arena_idx);
365+
if (arena == NULL) continue;
366+
for (size_t field_idx = 0; field_idx < arena->field_count; field_idx++) {
367+
size_t field = mi_atomic_load_relaxed(&arena->blocks_abandoned[field_idx]);
368+
while (field != 0) {
369+
const size_t bit_idx = mi_ctz(field);
370+
field &= ~((size_t)1 << bit_idx);
371+
const mi_bitmap_index_t bitmap_idx = mi_bitmap_index_create(field_idx, bit_idx);
372+
mi_segment_t* const segment = (mi_segment_t*)mi_arena_block_start(arena, bitmap_idx);
373+
mi_assert_internal(mi_atomic_load_relaxed(&segment->thread_id) == 0);
374+
if (segment->subproc != subproc) continue;
375+
if (!_mi_segment_visit_blocks(segment, heap_tag, visit_blocks, visitor, arg)) return false;
376+
}
377+
}
378+
}
379+
// segments outside arenas (OS allocated) are kept in a per sub-process list
380+
for (mi_segment_t* segment = subproc->abandoned_os_list; segment != NULL; segment = segment->abandoned_os_next) {
381+
mi_assert_internal(segment->subproc == subproc);
382+
if (!_mi_segment_visit_blocks(segment, heap_tag, visit_blocks, visitor, arg)) return false;
383+
}
384+
return true;
385+
}

‎Objects/mimalloc/free.c‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ static inline void mi_free_block_local(mi_page_t* page, mi_block_t* block, bool
3838
#if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN
3939
if (!mi_page_is_huge(page)) { // huge page content may be already decommitted
4040
size_t dbgsize = mi_page_block_size(page); if (dbgsize > 1*MI_MiB) { dbgsize = 1*MI_MiB; }
41-
memset(block, MI_DEBUG_FREED, dbgsize);
41+
mi_debug_fill(page, block, MI_DEBUG_FREED, dbgsize);
4242
}
4343
#endif
4444
if (track_stats) { mi_track_free_size(block, mi_page_usable_size_of(page, block, was_guarded)); } // faster then mi_usable_size as we already know the page and that p is unaligned
@@ -147,7 +147,7 @@ static inline mi_segment_t* mi_checked_ptr_segment(const void* p, const char* ms
147147
mi_segment_t* const segment = _mi_ptr_segment(p);
148148
if mi_unlikely(segment==NULL) return segment;
149149

150-
#if (MI_DEBUG>0)
150+
#if 0 && (MI_DEBUG>0) // CPython: disabled, the warning is noisy and not actionable for Python allocations
151151
if mi_unlikely(!mi_is_in_heap_region(p)) {
152152
#if (MI_INTPTR_SIZE == 8 && defined(__linux__))
153153
if (((uintptr_t)p >> 40) != 0x7F) { // linux tends to align large blocks above 0x7F000000000 (issue #640)
@@ -222,7 +222,10 @@ bool _mi_free_delayed_block(mi_block_t* block) {
222222
mi_assert_internal(block!=NULL);
223223
const mi_segment_t* const segment = _mi_ptr_segment(block);
224224
mi_assert_internal(_mi_ptr_cookie(segment) == segment->cookie);
225+
#ifndef Py_GIL_DISABLED
226+
// CPython: the GC traverses heaps of other threads, which can trigger this assert.
225227
mi_assert_internal(_mi_thread_id() == segment->thread_id);
228+
#endif
226229
mi_page_t* const page = _mi_segment_page_of(segment, block);
227230

228231
// Clear the no-delayed flag so delayed freeing is used again for this page.
@@ -340,7 +343,7 @@ static void mi_decl_noinline mi_free_block_mt(mi_page_t* page, mi_segment_t* seg
340343
else {
341344
#if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN // note: when tracking, cannot use mi_usable_size with multi-threading
342345
size_t dbgsize = mi_usable_size(block); if (dbgsize > 1*MI_MiB) { dbgsize = 1*MI_MiB; }
343-
memset(block, MI_DEBUG_FREED, dbgsize);
346+
mi_debug_fill(page, block, MI_DEBUG_FREED, dbgsize);
344347
#endif
345348
}
346349

‎Objects/mimalloc/heap.c‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ static bool mi_heap_page_collect(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t
104104
if (mi_page_all_free(page)) {
105105
// no more used blocks, free the page.
106106
// note: this will free retired pages as well.
107-
_mi_page_free(page, pq, collect >= MI_FORCE);
107+
bool freed = _PyMem_mi_page_maybe_free(page, pq, collect >= MI_FORCE);
108+
if (!freed && collect == MI_ABANDON) {
109+
// CPython: _PyMem_mi_page_maybe_free may have moved the page to a different
110+
// page queue, so we need to re-fetch the correct queue.
111+
size_t bin = (mi_page_is_in_full(page) ? MI_BIN_FULL : _mi_bin(mi_page_block_size(page)));
112+
_mi_page_abandon(page, &heap->pages[bin]);
113+
}
108114
}
109115
else if (collect == MI_ABANDON) {
110116
// still used blocks but the thread is done; abandon the page
@@ -164,6 +170,9 @@ static void mi_heap_collect_ex(mi_heap_t* heap, mi_collect_t collect)
164170
// collect retired pages
165171
_mi_heap_collect_retired(heap, force);
166172

173+
// CPython: free pages that were delayed with QSBR
174+
_PyMem_mi_heap_collect_qsbr(heap);
175+
167176
// collect all pages owned by this thread
168177
mi_heap_visit_pages(heap, &mi_heap_page_collect, (collect!=MI_NORMAL), &collect, NULL); // normally don't visit full pages, issue #1220
169178
mi_assert_internal( collect != MI_ABANDON || mi_atomic_load_ptr_acquire(mi_block_t,&heap->thread_delayed_free) == NULL );
@@ -741,6 +750,7 @@ static bool mi_heap_area_visitor(const mi_heap_t* heap, const mi_heap_area_ex_t*
741750
// Visit all blocks in a heap
742751
bool mi_heap_visit_blocks(const mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) {
743752
mi_visit_blocks_args_t args = { visit_blocks, visitor, arg };
753+
_mi_heap_delayed_free_partial((mi_heap_t *)heap); // CPython: gh-112532
744754
return mi_heap_visit_areas(heap, &mi_heap_area_visitor, &args);
745755
}
746756

‎Objects/mimalloc/init.c‎

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,7 @@ terms of the MIT license. A copy of the license can be found in the file
1313

1414

1515
// Empty page used to initialize the small free pages array
16-
const mi_page_t _mi_page_empty = {
17-
0,
18-
false, false, false, false,
19-
0, // capacity
20-
0, // reserved capacity
21-
{ 0 }, // flags
22-
false, // is_zero
23-
0, // retire_expire
24-
NULL, // free
25-
NULL, // local_free
26-
0, // used
27-
0, // block size shift
28-
0, // heap tag
29-
0, // block_size
30-
NULL, // page_start
31-
#if (MI_PADDING || MI_ENCODE_FREELIST)
32-
{ 0, 0 },
33-
#endif
34-
MI_ATOMIC_VAR_INIT(0), // xthread_free
35-
MI_ATOMIC_VAR_INIT(0), // xheap
36-
NULL, NULL
37-
, { 0 } // padding
38-
};
16+
const mi_page_t _mi_page_empty; // CPython: zero-initialized since mi_page_t has extra fields
3917

4018
#define MI_PAGE_EMPTY() ((mi_page_t*)&_mi_page_empty)
4119

@@ -124,6 +102,8 @@ mi_decl_cache_align const mi_heap_t _mi_heap_empty = {
124102
NULL, // next
125103
false, // can reclaim
126104
0, // tag
105+
0, // debug_offset (CPython)
106+
false, // page_use_qsbr (CPython)
127107
#if MI_GUARDED
128108
0, 0, 0, 1, // rate is 0 and count is 1 so we never write to it (see `internal.h:mi_heap_malloc_use_guarded`)
129109
#endif
@@ -176,6 +156,8 @@ mi_decl_cache_align mi_heap_t _mi_heap_main = {
176156
NULL, // next heap
177157
false, // can reclaim
178158
0, // tag
159+
0, // debug_offset (CPython)
160+
false, // page_use_qsbr (CPython)
179161
#if MI_GUARDED
180162
0, 0, 0, 0,
181163
#endif

0 commit comments

Comments
 (0)