Skip to content

Commit a751341

Browse files
author
Matt Page
committed
Drain each thread's page list during GC
Free pages awaiting QSBR are added to each thread's `mimalloc.page_list`. The list is only drained if the owning thread needs new pages, performs an allocation larger than mimalloc's medium-object limit, or the thread exits. This means that we don't free pages if a thread's allocation pattern quiesces or falls into a pattern that can be serviced by existing pages. This can be problematic for threads that allocate a bunch of memory and then settle into the previous pattern.
1 parent 01ff2e2 commit a751341

3 files changed

Lines changed: 49 additions & 7 deletions

File tree

‎Include/internal/pycore_pymem.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ extern void _PyMem_AbandonDelayed(PyThreadState *tstate);
129129
// On interpreter shutdown, frees all delayed free requests.
130130
extern void _PyMem_FiniDelayed(PyInterpreterState *interp);
131131

132+
// Free every empty page parked on each thread's page_list that has reached its
133+
// QSBR goal
134+
extern void _PyMem_FreeParkedQSBRPages(PyInterpreterState *interp);
135+
132136
#ifdef __cplusplus
133137
}
134138
#endif

‎Objects/obmalloc.c‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,26 @@ _PyMem_mi_page_reclaimed(mi_page_t *page)
226226
#endif
227227
}
228228

229+
#ifdef Py_GIL_DISABLED
230+
// Free the empty pages on `target`'s QSBR page list whose QSBR goal has been
231+
// reached.
232+
//
233+
// `cur` must be the calling thread's state: _Py_qsbr_poll() must be called
234+
// with the caller's own QSBR state. The goals themselves are global, so `cur`
235+
// can check pages parked by any thread.
236+
//
237+
// `target` is the thread whose page list is drained. It must be `cur`, or a
238+
// thread that is suspended (e.g., during a stop-the-world pause), because
239+
// _mi_page_free() modifies the heap and segment state of the thread that owns
240+
// the page.
229241
static void
230-
_PyMem_mi_heap_collect_qsbr(mi_heap_t *heap)
242+
_PyMem_mi_collect_tstate_page_list(_PyThreadStateImpl *cur, _PyThreadStateImpl *target)
231243
{
232-
#ifdef Py_GIL_DISABLED
233-
if (!heap->page_use_qsbr) {
244+
if (!_Py_atomic_load_int(&target->mimalloc.initialized)) {
245+
// The thread may not have called tstate_mimalloc_bind() yet.
234246
return;
235247
}
236-
237-
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET();
238-
struct llist_node *head = &tstate->mimalloc.page_list;
248+
struct llist_node *head = &target->mimalloc.page_list;
239249
if (llist_empty(head)) {
240250
return;
241251
}
@@ -249,13 +259,26 @@ _PyMem_mi_heap_collect_qsbr(mi_heap_t *heap)
249259
continue;
250260
}
251261

252-
if (!_Py_qsbr_poll(tstate->qsbr, page->qsbr_goal)) {
262+
if (!_Py_qsbr_poll(cur->qsbr, page->qsbr_goal)) {
253263
return;
254264
}
255265

256266
_PyMem_mi_page_clear_qsbr(page);
257267
_mi_page_free(page, mi_page_queue_of(page), false);
258268
}
269+
}
270+
#endif
271+
272+
static void
273+
_PyMem_mi_heap_collect_qsbr(mi_heap_t *heap)
274+
{
275+
#ifdef Py_GIL_DISABLED
276+
if (!heap->page_use_qsbr) {
277+
return;
278+
}
279+
280+
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET();
281+
_PyMem_mi_collect_tstate_page_list(tstate, tstate);
259282
#endif
260283
}
261284

@@ -1702,6 +1725,19 @@ _PyMem_FiniDelayed(PyInterpreterState *interp)
17021725
}
17031726
}
17041727

1728+
#ifdef Py_GIL_DISABLED
1729+
void
1730+
_PyMem_FreeParkedQSBRPages(PyInterpreterState *interp)
1731+
{
1732+
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *) _PyThreadState_GET();
1733+
_Py_FOR_EACH_TSTATE_BEGIN(interp, p) {
1734+
_PyMem_mi_collect_tstate_page_list(tstate, (_PyThreadStateImpl *) p);
1735+
}
1736+
_Py_FOR_EACH_TSTATE_END(interp);
1737+
}
1738+
#endif
1739+
1740+
17051741
/**************************/
17061742
/* the "object" allocator */
17071743
/**************************/

‎Python/gc_free_threading.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,6 +2110,8 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
21102110

21112111
process_delayed_frees(interp, state);
21122112

2113+
_PyMem_FreeParkedQSBRPages(interp);
2114+
21132115
#ifdef GC_ENABLE_MARK_ALIVE
21142116
// If gc.freeze() was used, it seems likely that doing this "mark alive"
21152117
// pass will not be a performance win. Typically the majority of alive

0 commit comments

Comments
 (0)