Skip to content

Commit 147cd05

Browse files
authored
GH-117760: Streamline the trashcan mechanism (GH-117763)
1 parent c917b3e commit 147cd05

File tree

5 files changed

+43
-128
lines changed

5 files changed

+43
-128
lines changed

Include/cpython/object.h

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,8 @@ without deallocating anything (and so unbounded call-stack depth is avoided).
448448
When the call stack finishes unwinding again, code generated by the END macro
449449
notices this, and calls another routine to deallocate all the objects that
450450
may have been added to the list of deferred deallocations. In effect, a
451-
chain of N deallocations is broken into (N-1)/(_PyTrash_UNWIND_LEVEL-1) pieces,
452-
with the call stack never exceeding a depth of _PyTrash_UNWIND_LEVEL.
451+
chain of N deallocations is broken into (N-1)/(Py_TRASHCAN_HEADROOM-1) pieces,
452+
with the call stack never exceeding a depth of Py_TRASHCAN_HEADROOM.
453453
454454
Since the tp_dealloc of a subclass typically calls the tp_dealloc of the base
455455
class, we need to ensure that the trashcan is only triggered on the tp_dealloc
@@ -461,30 +461,33 @@ passed as second argument to Py_TRASHCAN_BEGIN().
461461
/* Python 3.9 private API, invoked by the macros below. */
462462
PyAPI_FUNC(int) _PyTrash_begin(PyThreadState *tstate, PyObject *op);
463463
PyAPI_FUNC(void) _PyTrash_end(PyThreadState *tstate);
464+
465+
PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op);
466+
PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(PyThreadState *tstate);
467+
468+
464469
/* Python 3.10 private API, invoked by the Py_TRASHCAN_BEGIN(). */
465-
PyAPI_FUNC(int) _PyTrash_cond(PyObject *op, destructor dealloc);
466470

467-
#define Py_TRASHCAN_BEGIN_CONDITION(op, cond) \
468-
do { \
469-
PyThreadState *_tstate = NULL; \
470-
/* If "cond" is false, then _tstate remains NULL and the deallocator \
471-
* is run normally without involving the trashcan */ \
472-
if (cond) { \
473-
_tstate = PyThreadState_GetUnchecked(); \
474-
if (_PyTrash_begin(_tstate, _PyObject_CAST(op))) { \
475-
break; \
476-
} \
477-
}
478-
/* The body of the deallocator is here. */
479-
#define Py_TRASHCAN_END \
480-
if (_tstate) { \
481-
_PyTrash_end(_tstate); \
482-
} \
483-
} while (0);
471+
/* To avoid raising recursion errors during dealloc trigger trashcan before we reach
472+
* recursion limit. To avoid trashing, we don't attempt to empty the trashcan until
473+
* we have headroom above the trigger limit */
474+
#define Py_TRASHCAN_HEADROOM 50
484475

485476
#define Py_TRASHCAN_BEGIN(op, dealloc) \
486-
Py_TRASHCAN_BEGIN_CONDITION((op), \
487-
_PyTrash_cond(_PyObject_CAST(op), (destructor)(dealloc)))
477+
do { \
478+
PyThreadState *tstate = PyThreadState_Get(); \
479+
if (tstate->c_recursion_remaining <= Py_TRASHCAN_HEADROOM && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
480+
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); \
481+
break; \
482+
} \
483+
tstate->c_recursion_remaining--;
484+
/* The body of the deallocator is here. */
485+
#define Py_TRASHCAN_END \
486+
tstate->c_recursion_remaining++; \
487+
if (tstate->delete_later && tstate->c_recursion_remaining > (Py_TRASHCAN_HEADROOM*2)) { \
488+
_PyTrash_thread_destroy_chain(tstate); \
489+
} \
490+
} while (0);
488491

489492

490493
PyAPI_FUNC(void *) PyObject_GetItemData(PyObject *obj);

Include/cpython/pystate.h

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,6 @@ typedef struct _stack_chunk {
5656
PyObject * data[1]; /* Variable sized */
5757
} _PyStackChunk;
5858

59-
struct _py_trashcan {
60-
int delete_nesting;
61-
PyObject *delete_later;
62-
};
63-
6459
struct _ts {
6560
/* See Python/ceval.c for comments explaining most fields */
6661

@@ -152,7 +147,7 @@ struct _ts {
152147
*/
153148
unsigned long native_thread_id;
154149

155-
struct _py_trashcan trash;
150+
PyObject *delete_later;
156151

157152
/* Tagged pointer to top-most critical section, or zero if there is no
158153
* active critical section. Critical sections are only used in

Include/internal/pycore_object.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,6 @@ _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(PyObject *op)
615615
return (PyWeakReference **)((char *)op + offset);
616616
}
617617

618-
619618
// Fast inlined version of PyObject_IS_GC()
620619
static inline int
621620
_PyObject_IS_GC(PyObject *obj)

Objects/object.c

Lines changed: 15 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2709,33 +2709,31 @@ Py_ReprLeave(PyObject *obj)
27092709

27102710
/* Trashcan support. */
27112711

2712-
#define _PyTrash_UNWIND_LEVEL 50
2713-
27142712
/* Add op to the gcstate->trash_delete_later list. Called when the current
27152713
* call-stack depth gets large. op must be a currently untracked gc'ed
27162714
* object, with refcount 0. Py_DECREF must already have been called on it.
27172715
*/
2718-
static void
2719-
_PyTrash_thread_deposit_object(struct _py_trashcan *trash, PyObject *op)
2716+
void
2717+
_PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op)
27202718
{
27212719
_PyObject_ASSERT(op, _PyObject_IS_GC(op));
27222720
_PyObject_ASSERT(op, !_PyObject_GC_IS_TRACKED(op));
27232721
_PyObject_ASSERT(op, Py_REFCNT(op) == 0);
27242722
#ifdef Py_GIL_DISABLED
27252723
_PyObject_ASSERT(op, op->ob_tid == 0);
2726-
op->ob_tid = (uintptr_t)trash->delete_later;
2724+
op->ob_tid = (uintptr_t)tstate->delete_later;
27272725
#else
2728-
_PyGCHead_SET_PREV(_Py_AS_GC(op), (PyGC_Head*)trash->delete_later);
2726+
_PyGCHead_SET_PREV(_Py_AS_GC(op), (PyGC_Head*)tstate->delete_later);
27292727
#endif
2730-
trash->delete_later = op;
2728+
tstate->delete_later = op;
27312729
}
27322730

27332731
/* Deallocate all the objects in the gcstate->trash_delete_later list.
27342732
* Called when the call-stack unwinds again. */
2735-
static void
2736-
_PyTrash_thread_destroy_chain(struct _py_trashcan *trash)
2733+
void
2734+
_PyTrash_thread_destroy_chain(PyThreadState *tstate)
27372735
{
2738-
/* We need to increase trash_delete_nesting here, otherwise,
2736+
/* We need to increase c_recursion_remaining here, otherwise,
27392737
_PyTrash_thread_destroy_chain will be called recursively
27402738
and then possibly crash. An example that may crash without
27412739
increase:
@@ -2746,17 +2744,17 @@ _PyTrash_thread_destroy_chain(struct _py_trashcan *trash)
27462744
tups = [(tup,) for tup in tups]
27472745
del tups
27482746
*/
2749-
assert(trash->delete_nesting == 0);
2750-
++trash->delete_nesting;
2751-
while (trash->delete_later) {
2752-
PyObject *op = trash->delete_later;
2747+
assert(tstate->c_recursion_remaining > Py_TRASHCAN_HEADROOM);
2748+
tstate->c_recursion_remaining--;
2749+
while (tstate->delete_later) {
2750+
PyObject *op = tstate->delete_later;
27532751
destructor dealloc = Py_TYPE(op)->tp_dealloc;
27542752

27552753
#ifdef Py_GIL_DISABLED
2756-
trash->delete_later = (PyObject*) op->ob_tid;
2754+
tstate->delete_later = (PyObject*) op->ob_tid;
27572755
op->ob_tid = 0;
27582756
#else
2759-
trash->delete_later = (PyObject*) _PyGCHead_PREV(_Py_AS_GC(op));
2757+
tstate->delete_later = (PyObject*) _PyGCHead_PREV(_Py_AS_GC(op));
27602758
#endif
27612759

27622760
/* Call the deallocator directly. This used to try to
@@ -2767,92 +2765,10 @@ _PyTrash_thread_destroy_chain(struct _py_trashcan *trash)
27672765
*/
27682766
_PyObject_ASSERT(op, Py_REFCNT(op) == 0);
27692767
(*dealloc)(op);
2770-
assert(trash->delete_nesting == 1);
2771-
}
2772-
--trash->delete_nesting;
2773-
}
2774-
2775-
2776-
static struct _py_trashcan *
2777-
_PyTrash_get_state(PyThreadState *tstate)
2778-
{
2779-
if (tstate != NULL) {
2780-
return &tstate->trash;
2781-
}
2782-
// The current thread must be finalizing.
2783-
// Fall back to using thread-local state.
2784-
// XXX Use thread-local variable syntax?
2785-
assert(PyThread_tss_is_created(&_PyRuntime.trashTSSkey));
2786-
struct _py_trashcan *trash =
2787-
(struct _py_trashcan *)PyThread_tss_get(&_PyRuntime.trashTSSkey);
2788-
if (trash == NULL) {
2789-
trash = PyMem_RawMalloc(sizeof(struct _py_trashcan));
2790-
if (trash == NULL) {
2791-
Py_FatalError("Out of memory");
2792-
}
2793-
PyThread_tss_set(&_PyRuntime.trashTSSkey, (void *)trash);
2794-
}
2795-
return trash;
2796-
}
2797-
2798-
static void
2799-
_PyTrash_clear_state(PyThreadState *tstate)
2800-
{
2801-
if (tstate != NULL) {
2802-
assert(tstate->trash.delete_later == NULL);
2803-
return;
2804-
}
2805-
if (PyThread_tss_is_created(&_PyRuntime.trashTSSkey)) {
2806-
struct _py_trashcan *trash =
2807-
(struct _py_trashcan *)PyThread_tss_get(&_PyRuntime.trashTSSkey);
2808-
if (trash != NULL) {
2809-
PyThread_tss_set(&_PyRuntime.trashTSSkey, (void *)NULL);
2810-
PyMem_RawFree(trash);
2811-
}
28122768
}
2769+
tstate->c_recursion_remaining++;
28132770
}
28142771

2815-
2816-
int
2817-
_PyTrash_begin(PyThreadState *tstate, PyObject *op)
2818-
{
2819-
// XXX Make sure the GIL is held.
2820-
struct _py_trashcan *trash = _PyTrash_get_state(tstate);
2821-
if (trash->delete_nesting >= _PyTrash_UNWIND_LEVEL) {
2822-
/* Store the object (to be deallocated later) and jump past
2823-
* Py_TRASHCAN_END, skipping the body of the deallocator */
2824-
_PyTrash_thread_deposit_object(trash, op);
2825-
return 1;
2826-
}
2827-
++trash->delete_nesting;
2828-
return 0;
2829-
}
2830-
2831-
2832-
void
2833-
_PyTrash_end(PyThreadState *tstate)
2834-
{
2835-
// XXX Make sure the GIL is held.
2836-
struct _py_trashcan *trash = _PyTrash_get_state(tstate);
2837-
--trash->delete_nesting;
2838-
if (trash->delete_nesting <= 0) {
2839-
if (trash->delete_later != NULL) {
2840-
_PyTrash_thread_destroy_chain(trash);
2841-
}
2842-
_PyTrash_clear_state(tstate);
2843-
}
2844-
}
2845-
2846-
2847-
/* bpo-40170: It's only be used in Py_TRASHCAN_BEGIN macro to hide
2848-
implementation details. */
2849-
int
2850-
_PyTrash_cond(PyObject *op, destructor dealloc)
2851-
{
2852-
return Py_TYPE(op)->tp_dealloc == dealloc;
2853-
}
2854-
2855-
28562772
void _Py_NO_RETURN
28572773
_PyObject_AssertFailed(PyObject *obj, const char *expr, const char *msg,
28582774
const char *file, int line, const char *function)

Python/pystate.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,8 @@ init_threadstate(_PyThreadStateImpl *_tstate,
14851485
tstate->what_event = -1;
14861486
tstate->previous_executor = NULL;
14871487

1488+
tstate->delete_later = NULL;
1489+
14881490
llist_init(&_tstate->mem_free_queue);
14891491

14901492
if (interp->stoptheworld.requested || _PyRuntime.stoptheworld.requested) {

0 commit comments

Comments
 (0)