From 20afb79a03c94703740b02c8f9a7d3b8041f0c7f Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Sat, 3 Oct 2026 19:23:33 +0900 Subject: [PATCH 1/3] gh-130706: Add a fast path to _Py_Dealloc() for non-GC objects. --- ...-10-03-19-11-03.gh-issue-130706.01-O6U.rst | 2 ++ Objects/object.c | 20 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-19-11-03.gh-issue-130706.01-O6U.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-19-11-03.gh-issue-130706.01-O6U.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-19-11-03.gh-issue-130706.01-O6U.rst new file mode 100644 index 000000000000000..3351513347daecd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-19-11-03.gh-issue-130706.01-O6U.rst @@ -0,0 +1,2 @@ +Add a fast path to ``_Py_Dealloc()`` for non-GC objects. Patch by Donghee +Na. diff --git a/Objects/object.c b/Objects/object.c index c7aeba0cee22c68..d67de54fda0157a 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3306,8 +3306,8 @@ next" object in the chain to 0. This can easily lead to stack overflows. To avoid that, if the C stack is nearing its limit, instead of calling dealloc on the object, it is added to a queue to be freed later when the stack is shallower */ -void -_Py_Dealloc(PyObject *op) +static Py_NO_INLINE void +dealloc_general(PyObject *op) { PyTypeObject *type = Py_TYPE(op); unsigned long gc_flag = type->tp_flags & Py_TPFLAGS_HAVE_GC; @@ -3372,6 +3372,22 @@ _Py_Dealloc(PyObject *op) } } +void +_Py_Dealloc(PyObject *op) +{ +#if !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) + // gh-130706: Remove unnecessary stack frame for non-GC objects. + PyTypeObject *type = Py_TYPE(op); + if (_PyRuntime.ref_tracer.tracer_func == NULL + && !(type->tp_flags & Py_TPFLAGS_HAVE_GC)) + { + type->tp_dealloc(op); + return; + } +#endif + dealloc_general(op); +} + PyObject ** PyObject_GET_WEAKREFS_LISTPTR(PyObject *op) From c966005eb79cbe161aa6d0b872954d26e1a229d0 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Sat, 3 Oct 2026 19:28:47 +0900 Subject: [PATCH 2/3] Add comment --- Objects/object.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/object.c b/Objects/object.c index d67de54fda0157a..20eb142f65eed2a 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3385,6 +3385,7 @@ _Py_Dealloc(PyObject *op) return; } #endif + // GC objects (trashcan), reftracer set, or debug builds. dealloc_general(op); } From 474fed85628f4b5ba94483ca5ebc14b1089dd738 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Sun, 4 Oct 2026 09:46:00 +0900 Subject: [PATCH 3/3] Address code review --- Objects/object.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index 20eb142f65eed2a..e334097d31c4325 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3307,7 +3307,7 @@ To avoid that, if the C stack is nearing its limit, instead of calling dealloc on the object, it is added to a queue to be freed later when the stack is shallower */ static Py_NO_INLINE void -dealloc_general(PyObject *op) +py_dealloc(PyObject *op) { PyTypeObject *type = Py_TYPE(op); unsigned long gc_flag = type->tp_flags & Py_TPFLAGS_HAVE_GC; @@ -3372,11 +3372,16 @@ dealloc_general(PyObject *op) } } +/* + * gh-130706: Keep the GC/reftracer path in the non-inlined py_dealloc(). + * Inlining it makes the compiler save callee-saved registers at entry, + * so the non-GC objects path would have unnecessary register spills. + */ void _Py_Dealloc(PyObject *op) { #if !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) - // gh-130706: Remove unnecessary stack frame for non-GC objects. + // gh-130706: Avoid unnecessary register spills for non-GC objects. PyTypeObject *type = Py_TYPE(op); if (_PyRuntime.ref_tracer.tracer_func == NULL && !(type->tp_flags & Py_TPFLAGS_HAVE_GC)) @@ -3386,7 +3391,7 @@ _Py_Dealloc(PyObject *op) } #endif // GC objects (trashcan), reftracer set, or debug builds. - dealloc_general(op); + py_dealloc(op); }