From a9edb7b7974507c7ae23dfbd8edd7c0025e74a5a Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 28 Nov 2025 20:27:14 +0000 Subject: [PATCH 1/6] reset allocation counts in gc thread-local buffers --- Python/gc_free_threading.c | 9 ++++----- Python/pystate.c | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 1717603b947f90..7d8601f0c2f128 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -2207,11 +2207,10 @@ record_deallocation(PyThreadState *tstate) { struct _gc_thread_state *gc = &((_PyThreadStateImpl *)tstate)->gc; - gc->alloc_count--; - if (gc->alloc_count <= -LOCAL_ALLOC_COUNT_THRESHOLD) { - GCState *gcstate = &tstate->interp->gc; - _Py_atomic_add_int(&gcstate->young.count, (int)gc->alloc_count); - gc->alloc_count = 0; + // Only decrement if positive, matching gc.c behavior which prevents + // negative counts (see PyObject_GC_Del in gc.c). + if (gc->alloc_count > 0) { + gc->alloc_count--; } } diff --git a/Python/pystate.c b/Python/pystate.c index c12a1418e74309..5450f63ceea46f 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1810,6 +1810,14 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) assert(tstate_impl->refcounts.values == NULL); #endif +#ifdef Py_GIL_DISABLED + // Flush the thread's local GC allocation count to the global count + // before the thread state is deleted, otherwise the count is lost. + _Py_atomic_add_int(&tstate->interp->gc.young.count, + (int)((_PyThreadStateImpl *)tstate)->gc.alloc_count); + ((_PyThreadStateImpl *)tstate)->gc.alloc_count = 0; +#endif + #if _Py_TIER2 _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; if (_tstate->jit_tracer_state.code_buffer != NULL) { From 9388518613c625629ca3f289561743f7fd952de1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 1 Dec 2025 12:23:12 -0800 Subject: [PATCH 2/6] Fix allocation count decrement and GC state update Adjust allocation count handling to prevent negative values and update GC state accordingly. --- Python/gc_free_threading.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 7d8601f0c2f128..31356d9ec79274 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -2207,10 +2207,23 @@ record_deallocation(PyThreadState *tstate) { struct _gc_thread_state *gc = &((_PyThreadStateImpl *)tstate)->gc; - // Only decrement if positive, matching gc.c behavior which prevents - // negative counts (see PyObject_GC_Del in gc.c). - if (gc->alloc_count > 0) { - gc->alloc_count--; + gc->alloc_count--; + if (gc->alloc_count <= -LOCAL_ALLOC_COUNT_THRESHOLD) { + GCState *gcstate = &tstate->interp->gc; + int count = _Py_atomic_load_int_relaxed(&gcstate->young.count); + int new_count; + do { + if (count == 0){ + break; + } + new_count = count + (int)gc->alloc_count; + if (new_count < 0) { + new_count = 0; + } + } while (!_Py_atomic_compare_exchange_int(&gcstate->young.count, + &count, + new_count)); + gc->alloc_count = 0; } } From f5e46b5818f1fc147d4d1d0f8523ad4a2d98b3b1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 1 Dec 2025 12:25:47 -0800 Subject: [PATCH 3/6] Fix formatting in gc_free_threading.c --- Python/gc_free_threading.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 31356d9ec79274..3d30c00f4abe26 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -2213,7 +2213,7 @@ record_deallocation(PyThreadState *tstate) int count = _Py_atomic_load_int_relaxed(&gcstate->young.count); int new_count; do { - if (count == 0){ + if (count == 0) { break; } new_count = count + (int)gc->alloc_count; From 795e703759b4928edd7dc87a161fe5e3ad35d6e9 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 1 Dec 2025 12:34:13 -0800 Subject: [PATCH 4/6] Update Python/pystate.c Co-authored-by: Sam Gross --- Python/pystate.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index 5450f63ceea46f..5b5b0a5f76a12f 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1814,8 +1814,8 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) // Flush the thread's local GC allocation count to the global count // before the thread state is deleted, otherwise the count is lost. _Py_atomic_add_int(&tstate->interp->gc.young.count, - (int)((_PyThreadStateImpl *)tstate)->gc.alloc_count); - ((_PyThreadStateImpl *)tstate)->gc.alloc_count = 0; + (int)tstate_impl->gc.alloc_count); + tstate_impl->gc.alloc_count = 0; #endif #if _Py_TIER2 From 8d01b65df5a0c21ef68cdfe955f5257190424ae6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 1 Dec 2025 20:41:42 +0000 Subject: [PATCH 5/6] news and lint --- .../2025-12-01-20-41-26.gh-issue-142048.c2YosX.rst | 2 ++ Python/gc_free_threading.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-01-20-41-26.gh-issue-142048.c2YosX.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-01-20-41-26.gh-issue-142048.c2YosX.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-01-20-41-26.gh-issue-142048.c2YosX.rst new file mode 100644 index 00000000000000..1400dae13ffe32 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-01-20-41-26.gh-issue-142048.c2YosX.rst @@ -0,0 +1,2 @@ +Fix quadratically increasing garbage collection delays in free-threaded +build. diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 3d30c00f4abe26..e672e870db2f27 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -2213,7 +2213,7 @@ record_deallocation(PyThreadState *tstate) int count = _Py_atomic_load_int_relaxed(&gcstate->young.count); int new_count; do { - if (count == 0) { + if (count == 0) { break; } new_count = count + (int)gc->alloc_count; From 8be49b5f1f78efc60bbad28ffb80112a1a302c05 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 1 Dec 2025 14:27:41 -0800 Subject: [PATCH 6/6] Remove local GC allocation count flush in pystate.c Removed flushing of thread's local GC allocation count before thread state deletion. --- Python/pystate.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index 5b5b0a5f76a12f..c12a1418e74309 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1810,14 +1810,6 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) assert(tstate_impl->refcounts.values == NULL); #endif -#ifdef Py_GIL_DISABLED - // Flush the thread's local GC allocation count to the global count - // before the thread state is deleted, otherwise the count is lost. - _Py_atomic_add_int(&tstate->interp->gc.young.count, - (int)tstate_impl->gc.alloc_count); - tstate_impl->gc.alloc_count = 0; -#endif - #if _Py_TIER2 _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; if (_tstate->jit_tracer_state.code_buffer != NULL) {