Skip to content

Commit c4ef5f6

Browse files
johngclaude
andcommitted
gh-154429: Skip fetching the thread state for non-GC types in _Py_Dealloc
_Py_Dealloc() fetched the current thread state and computed the C recursion margin on every deallocation, but both are only used by the trashcan mechanism, which applies only to GC-tracked types. Gate them behind the Py_TPFLAGS_HAVE_GC check so the common non-GC case (int, float, str and similar atomic types) skips them. On platforms where _PyThreadState_GET() reads a _Thread_local via a function call (e.g. macOS TLV), this removes a call from every non-GC deallocation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9d231cb commit c4ef5f6

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Speed up deallocation of objects that are not tracked by the garbage
2+
collector (such as :class:`int`, :class:`float` and :class:`str`) by skipping
3+
the thread-state fetch and recursion-margin computation in the object
4+
deallocator. These are only needed by the trashcan mechanism, which applies
5+
only to GC-tracked types. On platforms where the current thread state is
6+
accessed through a function call (for example macOS), this removes a call from
7+
every non-GC deallocation.

Objects/object.c

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3292,13 +3292,29 @@ _Py_Dealloc(PyObject *op)
32923292
PyTypeObject *type = Py_TYPE(op);
32933293
unsigned long gc_flag = type->tp_flags & Py_TPFLAGS_HAVE_GC;
32943294
destructor dealloc = type->tp_dealloc;
3295-
PyThreadState *tstate = _PyThreadState_GET();
3296-
intptr_t margin = _Py_RecursionLimit_GetMargin(tstate);
3297-
if (margin < 2 && gc_flag) {
3298-
_PyTrash_thread_deposit_object(tstate, (PyObject *)op);
3299-
return;
3295+
// The trashcan mechanism -- and the thread state and recursion margin it
3296+
// relies on -- only applies to GC-tracked types: only they can form the
3297+
// deep deallocation chains the trashcan guards against. Skip fetching the
3298+
// thread state for the common non-GC case (float/int/str temporaries and
3299+
// the like). On platforms where the thread state lives in a thread-local
3300+
// accessed via a function call (e.g. macOS TLV), this removes a call from
3301+
// every non-GC deallocation.
3302+
PyThreadState *tstate = NULL;
3303+
intptr_t margin = 0;
3304+
if (gc_flag) {
3305+
tstate = _PyThreadState_GET();
3306+
margin = _Py_RecursionLimit_GetMargin(tstate);
3307+
if (margin < 2) {
3308+
_PyTrash_thread_deposit_object(tstate, (PyObject *)op);
3309+
return;
3310+
}
33003311
}
33013312
#ifdef Py_DEBUG
3313+
// The non-GC fast path above skips fetching the thread state; the debug
3314+
// bookkeeping below needs it, so fetch it now if we haven't already.
3315+
if (tstate == NULL) {
3316+
tstate = _PyThreadState_GET();
3317+
}
33023318
#if !defined(Py_GIL_DISABLED) && !defined(Py_STACKREF_DEBUG)
33033319
/* This assertion doesn't hold for the free-threading build, as
33043320
* PyStackRef_CLOSE_SPECIALIZED is not implemented */
@@ -3340,7 +3356,9 @@ _Py_Dealloc(PyObject *op)
33403356
Py_XDECREF(old_exc);
33413357
Py_DECREF(type);
33423358
#endif
3343-
if (tstate->delete_later && margin >= 4 && gc_flag) {
3359+
// gc_flag is checked first so tstate (NULL on the non-GC fast path) is
3360+
// only dereferenced for GC-tracked types, where it was fetched above.
3361+
if (gc_flag && tstate->delete_later && margin >= 4) {
33443362
_PyTrash_thread_destroy_chain(tstate);
33453363
}
33463364
}

0 commit comments

Comments
 (0)