Skip to content

Commit 47c2439

Browse files
committed
Move trashcan mechanism into Py_Dealloc
1 parent c5e856a commit 47c2439

19 files changed

+23
-115
lines changed

Include/cpython/object.h

+3-70
Original file line numberDiff line numberDiff line change
@@ -429,81 +429,14 @@ PyAPI_FUNC(void) _Py_NO_RETURN _PyObject_AssertFailed(
429429
const char *function);
430430

431431

432-
/* Trashcan mechanism, thanks to Christian Tismer.
433-
434-
When deallocating a container object, it's possible to trigger an unbounded
435-
chain of deallocations, as each Py_DECREF in turn drops the refcount on "the
436-
next" object in the chain to 0. This can easily lead to stack overflows,
437-
especially in threads (which typically have less stack space to work with).
438-
439-
A container object can avoid this by bracketing the body of its tp_dealloc
440-
function with a pair of macros:
441-
442-
static void
443-
mytype_dealloc(mytype *p)
444-
{
445-
... declarations go here ...
446-
447-
PyObject_GC_UnTrack(p); // must untrack first
448-
Py_TRASHCAN_BEGIN(p, mytype_dealloc)
449-
... The body of the deallocator goes here, including all calls ...
450-
... to Py_DECREF on contained objects. ...
451-
Py_TRASHCAN_END // there should be no code after this
452-
}
453-
454-
CAUTION: Never return from the middle of the body! If the body needs to
455-
"get out early", put a label immediately before the Py_TRASHCAN_END
456-
call, and goto it. Else the call-depth counter (see below) will stay
457-
above 0 forever, and the trashcan will never get emptied.
458-
459-
How it works: The BEGIN macro increments a call-depth counter. So long
460-
as this counter is small, the body of the deallocator is run directly without
461-
further ado. But if the counter gets large, it instead adds p to a list of
462-
objects to be deallocated later, skips the body of the deallocator, and
463-
resumes execution after the END macro. The tp_dealloc routine then returns
464-
without deallocating anything (and so unbounded call-stack depth is avoided).
465-
466-
When the call stack finishes unwinding again, code generated by the END macro
467-
notices this, and calls another routine to deallocate all the objects that
468-
may have been added to the list of deferred deallocations. In effect, a
469-
chain of N deallocations is broken into (N-1)/(Py_TRASHCAN_HEADROOM-1) pieces,
470-
with the call stack never exceeding a depth of Py_TRASHCAN_HEADROOM.
471-
472-
Since the tp_dealloc of a subclass typically calls the tp_dealloc of the base
473-
class, we need to ensure that the trashcan is only triggered on the tp_dealloc
474-
of the actual class being deallocated. Otherwise we might end up with a
475-
partially-deallocated object. To check this, the tp_dealloc function must be
476-
passed as second argument to Py_TRASHCAN_BEGIN().
477-
*/
478-
479-
480432
PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op);
481433
PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(PyThreadState *tstate);
482434

483-
484-
/* Python 3.10 private API, invoked by the Py_TRASHCAN_BEGIN(). */
485-
486-
/* To avoid raising recursion errors during dealloc trigger trashcan before we reach
487-
* recursion limit. To avoid trashing, we don't attempt to empty the trashcan until
488-
* we have headroom above the trigger limit */
489-
#define Py_TRASHCAN_HEADROOM 50
490-
491-
/* Helper function for Py_TRASHCAN_BEGIN */
492435
PyAPI_FUNC(int) _Py_ReachedRecursionLimitWithMargin(PyThreadState *tstate, int margin_count);
493436

494-
#define Py_TRASHCAN_BEGIN(op, dealloc) \
495-
do { \
496-
PyThreadState *tstate = PyThreadState_Get(); \
497-
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2) && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
498-
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); \
499-
break; \
500-
}
501-
/* The body of the deallocator is here. */
502-
#define Py_TRASHCAN_END \
503-
if (tstate->delete_later && !_Py_ReachedRecursionLimitWithMargin(tstate, 4)) { \
504-
_PyTrash_thread_destroy_chain(tstate); \
505-
} \
506-
} while (0);
437+
/* For backwards compatibility with the old trashcan mechanism */
438+
#define Py_TRASHCAN_BEGIN(op, dealloc)
439+
#define Py_TRASHCAN_END
507440

508441

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

Modules/_elementtree.c

-2
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,6 @@ element_dealloc(PyObject *op)
689689

690690
/* bpo-31095: UnTrack is needed before calling any callbacks */
691691
PyObject_GC_UnTrack(self);
692-
Py_TRASHCAN_BEGIN(self, element_dealloc)
693692

694693
if (self->weakreflist != NULL)
695694
PyObject_ClearWeakRefs(op);
@@ -700,7 +699,6 @@ element_dealloc(PyObject *op)
700699

701700
tp->tp_free(self);
702701
Py_DECREF(tp);
703-
Py_TRASHCAN_END
704702
}
705703

706704
/* -------------------------------------------------------------------- */

Objects/descrobject.c

-2
Original file line numberDiff line numberDiff line change
@@ -1311,11 +1311,9 @@ wrapper_dealloc(PyObject *self)
13111311
{
13121312
wrapperobject *wp = (wrapperobject *)self;
13131313
PyObject_GC_UnTrack(wp);
1314-
Py_TRASHCAN_BEGIN(wp, wrapper_dealloc)
13151314
Py_XDECREF(wp->descr);
13161315
Py_XDECREF(wp->self);
13171316
PyObject_GC_Del(wp);
1318-
Py_TRASHCAN_END
13191317
}
13201318

13211319
static PyObject *

Objects/dictobject.c

-2
Original file line numberDiff line numberDiff line change
@@ -3262,7 +3262,6 @@ dict_dealloc(PyObject *self)
32623262

32633263
/* bpo-31095: UnTrack is needed before calling any callbacks */
32643264
PyObject_GC_UnTrack(mp);
3265-
Py_TRASHCAN_BEGIN(mp, dict_dealloc)
32663265
if (values != NULL) {
32673266
if (values->embedded == 0) {
32683267
for (i = 0, n = values->capacity; i < n; i++) {
@@ -3282,7 +3281,6 @@ dict_dealloc(PyObject *self)
32823281
else {
32833282
Py_TYPE(mp)->tp_free((PyObject *)mp);
32843283
}
3285-
Py_TRASHCAN_END
32863284
}
32873285

32883286

Objects/exceptions.c

-2
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,8 @@ BaseException_dealloc(PyObject *op)
150150
// bpo-44348: The trashcan mechanism prevents stack overflow when deleting
151151
// long chains of exceptions. For example, exceptions can be chained
152152
// through the __context__ attributes or the __traceback__ attribute.
153-
Py_TRASHCAN_BEGIN(self, BaseException_dealloc)
154153
(void)BaseException_clear(op);
155154
Py_TYPE(self)->tp_free(self);
156-
Py_TRASHCAN_END
157155
}
158156

159157
static int

Objects/frameobject.c

-2
Original file line numberDiff line numberDiff line change
@@ -1916,7 +1916,6 @@ frame_dealloc(PyObject *op)
19161916
_PyObject_GC_UNTRACK(f);
19171917
}
19181918

1919-
Py_TRASHCAN_BEGIN(f, frame_dealloc);
19201919
/* GH-106092: If f->f_frame was on the stack and we reached the maximum
19211920
* nesting depth for deallocations, the trashcan may have delayed this
19221921
* deallocation until after f->f_frame is freed. Avoid dereferencing
@@ -1941,7 +1940,6 @@ frame_dealloc(PyObject *op)
19411940
Py_CLEAR(f->f_locals_cache);
19421941
Py_CLEAR(f->f_overwritten_fast_locals);
19431942
PyObject_GC_Del(f);
1944-
Py_TRASHCAN_END;
19451943
}
19461944

19471945
static int

Objects/listobject.c

-2
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,6 @@ list_dealloc(PyObject *self)
550550
PyListObject *op = (PyListObject *)self;
551551
Py_ssize_t i;
552552
PyObject_GC_UnTrack(op);
553-
Py_TRASHCAN_BEGIN(op, list_dealloc)
554553
if (op->ob_item != NULL) {
555554
/* Do it backwards, for Christian Tismer.
556555
There's a simple test case where somehow this reduces
@@ -569,7 +568,6 @@ list_dealloc(PyObject *self)
569568
else {
570569
PyObject_GC_Del(op);
571570
}
572-
Py_TRASHCAN_END
573571
}
574572

575573
static PyObject *

Objects/methodobject.c

-4
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,7 @@ static void
166166
meth_dealloc(PyObject *self)
167167
{
168168
PyCFunctionObject *m = _PyCFunctionObject_CAST(self);
169-
// The Py_TRASHCAN mechanism requires that we be able to
170-
// call PyObject_GC_UnTrack twice on an object.
171169
PyObject_GC_UnTrack(m);
172-
Py_TRASHCAN_BEGIN(m, meth_dealloc);
173170
if (m->m_weakreflist != NULL) {
174171
PyObject_ClearWeakRefs((PyObject*) m);
175172
}
@@ -186,7 +183,6 @@ meth_dealloc(PyObject *self)
186183
assert(Py_IS_TYPE(self, &PyCFunction_Type));
187184
_Py_FREELIST_FREE(pycfunctionobject, m, PyObject_GC_Del);
188185
}
189-
Py_TRASHCAN_END;
190186
}
191187

192188
static PyObject *

Objects/object.c

+18-5
Original file line numberDiff line numberDiff line change
@@ -2908,13 +2908,11 @@ Py_ReprLeave(PyObject *obj)
29082908
void
29092909
_PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op)
29102910
{
2911-
_PyObject_ASSERT(op, _PyObject_IS_GC(op));
2912-
_PyObject_ASSERT(op, !_PyObject_GC_IS_TRACKED(op));
29132911
_PyObject_ASSERT(op, Py_REFCNT(op) == 0);
29142912
#ifdef Py_GIL_DISABLED
29152913
op->ob_tid = (uintptr_t)tstate->delete_later;
29162914
#else
2917-
_PyGCHead_SET_PREV(_Py_AS_GC(op), (PyGC_Head*)tstate->delete_later);
2915+
op->ob_refcnt_full = (intptr_t)tstate->delete_later;
29182916
#endif
29192917
tstate->delete_later = op;
29202918
}
@@ -2933,7 +2931,8 @@ _PyTrash_thread_destroy_chain(PyThreadState *tstate)
29332931
op->ob_tid = 0;
29342932
_Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, _Py_REF_MERGED);
29352933
#else
2936-
tstate->delete_later = (PyObject*) _PyGCHead_PREV(_Py_AS_GC(op));
2934+
tstate->delete_later = (PyObject*) op->ob_refcnt_full;
2935+
op->ob_refcnt_full = 0;
29372936
#endif
29382937

29392938
/* Call the deallocator directly. This used to try to
@@ -2998,13 +2997,24 @@ _PyObject_AssertFailed(PyObject *obj, const char *expr, const char *msg,
29982997
}
29992998

30002999

3000+
/*
3001+
When deallocating a container object, it's possible to trigger an unbounded
3002+
chain of deallocations, as each Py_DECREF in turn drops the refcount on "the
3003+
next" object in the chain to 0. This can easily lead to stack overflows.
3004+
To avoid that, if the C stack is nearing its limit, instead of calling
3005+
dealloc on the object, it is added to a queue to be freed later when the
3006+
stack is shallower */
30013007
void
30023008
_Py_Dealloc(PyObject *op)
30033009
{
30043010
PyTypeObject *type = Py_TYPE(op);
30053011
destructor dealloc = type->tp_dealloc;
3006-
#ifdef Py_DEBUG
30073012
PyThreadState *tstate = _PyThreadState_GET();
3013+
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) {
3014+
_PyTrash_thread_deposit_object(tstate, (PyObject *)op);
3015+
return;
3016+
}
3017+
#ifdef Py_DEBUG
30083018
#if !defined(Py_GIL_DISABLED) && !defined(Py_STACKREF_DEBUG)
30093019
/* This assertion doesn't hold for the free-threading build, as
30103020
* PyStackRef_CLOSE_SPECIALIZED is not implemented */
@@ -3046,6 +3056,9 @@ _Py_Dealloc(PyObject *op)
30463056
Py_XDECREF(old_exc);
30473057
Py_DECREF(type);
30483058
#endif
3059+
if (tstate->delete_later && !_Py_ReachedRecursionLimitWithMargin(tstate, 4)) {
3060+
_PyTrash_thread_destroy_chain(tstate);
3061+
}
30493062
}
30503063

30513064

Objects/odictobject.c

-3
Original file line numberDiff line numberDiff line change
@@ -1389,16 +1389,13 @@ odict_dealloc(PyObject *op)
13891389
{
13901390
PyODictObject *self = _PyODictObject_CAST(op);
13911391
PyObject_GC_UnTrack(self);
1392-
Py_TRASHCAN_BEGIN(self, odict_dealloc)
13931392

13941393
Py_XDECREF(self->od_inst_dict);
13951394
if (self->od_weakreflist != NULL)
13961395
PyObject_ClearWeakRefs((PyObject *)self);
13971396

13981397
_odict_clear_nodes(self);
13991398
PyDict_Type.tp_dealloc((PyObject *)self);
1400-
1401-
Py_TRASHCAN_END
14021399
}
14031400

14041401
/* tp_repr */

Objects/setobject.c

-2
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,6 @@ set_dealloc(PyObject *self)
516516

517517
/* bpo-31095: UnTrack is needed before calling any callbacks */
518518
PyObject_GC_UnTrack(so);
519-
Py_TRASHCAN_BEGIN(so, set_dealloc)
520519
if (so->weakreflist != NULL)
521520
PyObject_ClearWeakRefs((PyObject *) so);
522521

@@ -529,7 +528,6 @@ set_dealloc(PyObject *self)
529528
if (so->table != so->smalltable)
530529
PyMem_Free(so->table);
531530
Py_TYPE(so)->tp_free(so);
532-
Py_TRASHCAN_END
533531
}
534532

535533
static PyObject *

Objects/tupleobject.c

-3
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ tuple_dealloc(PyObject *self)
207207
}
208208

209209
PyObject_GC_UnTrack(op);
210-
Py_TRASHCAN_BEGIN(op, tuple_dealloc)
211210

212211
Py_ssize_t i = Py_SIZE(op);
213212
while (--i >= 0) {
@@ -217,8 +216,6 @@ tuple_dealloc(PyObject *self)
217216
if (!maybe_freelist_push(op)) {
218217
Py_TYPE(op)->tp_free((PyObject *)op);
219218
}
220-
221-
Py_TRASHCAN_END
222219
}
223220

224221
static PyObject *

Objects/typeobject.c

-2
Original file line numberDiff line numberDiff line change
@@ -2555,7 +2555,6 @@ subtype_dealloc(PyObject *self)
25552555
/* UnTrack and re-Track around the trashcan macro, alas */
25562556
/* See explanation at end of function for full disclosure */
25572557
PyObject_GC_UnTrack(self);
2558-
Py_TRASHCAN_BEGIN(self, subtype_dealloc);
25592558

25602559
/* Find the nearest base with a different tp_dealloc */
25612560
base = type;
@@ -2657,7 +2656,6 @@ subtype_dealloc(PyObject *self)
26572656
}
26582657

26592658
endlabel:
2660-
Py_TRASHCAN_END
26612659

26622660
/* Explanation of the weirdness around the trashcan macros:
26632661

Python/bltinmodule.c

-2
Original file line numberDiff line numberDiff line change
@@ -566,11 +566,9 @@ filter_dealloc(PyObject *self)
566566
{
567567
filterobject *lz = _filterobject_CAST(self);
568568
PyObject_GC_UnTrack(lz);
569-
Py_TRASHCAN_BEGIN(lz, filter_dealloc)
570569
Py_XDECREF(lz->func);
571570
Py_XDECREF(lz->it);
572571
Py_TYPE(lz)->tp_free(lz);
573-
Py_TRASHCAN_END
574572
}
575573

576574
static int

Python/gc.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -2201,7 +2201,7 @@ void
22012201
PyObject_GC_UnTrack(void *op_raw)
22022202
{
22032203
PyObject *op = _PyObject_CAST(op_raw);
2204-
/* Obscure: the Py_TRASHCAN mechanism requires that we be able to
2204+
/* Obscure: the trashcan mechanism requires that we be able to
22052205
* call PyObject_GC_UnTrack twice on an object.
22062206
*/
22072207
if (_PyObject_GC_IS_TRACKED(op)) {

Python/gc_free_threading.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -2500,7 +2500,7 @@ void
25002500
PyObject_GC_UnTrack(void *op_raw)
25012501
{
25022502
PyObject *op = _PyObject_CAST(op_raw);
2503-
/* Obscure: the Py_TRASHCAN mechanism requires that we be able to
2503+
/* Obscure: the trashcan mechanism requires that we be able to
25042504
* call PyObject_GC_UnTrack twice on an object.
25052505
*/
25062506
if (_PyObject_GC_IS_TRACKED(op)) {

Python/hamt.c

-6
Original file line numberDiff line numberDiff line change
@@ -1118,7 +1118,6 @@ hamt_node_bitmap_dealloc(PyObject *self)
11181118
}
11191119

11201120
PyObject_GC_UnTrack(self);
1121-
Py_TRASHCAN_BEGIN(self, hamt_node_bitmap_dealloc)
11221121

11231122
if (len > 0) {
11241123
i = len;
@@ -1128,7 +1127,6 @@ hamt_node_bitmap_dealloc(PyObject *self)
11281127
}
11291128

11301129
Py_TYPE(self)->tp_free(self);
1131-
Py_TRASHCAN_END
11321130
}
11331131

11341132
#ifdef Py_DEBUG
@@ -1508,15 +1506,13 @@ hamt_node_collision_dealloc(PyObject *self)
15081506
/* Collision's tp_dealloc */
15091507
Py_ssize_t len = Py_SIZE(self);
15101508
PyObject_GC_UnTrack(self);
1511-
Py_TRASHCAN_BEGIN(self, hamt_node_collision_dealloc)
15121509
if (len > 0) {
15131510
PyHamtNode_Collision *node = _PyHamtNode_Collision_CAST(self);
15141511
while (--len >= 0) {
15151512
Py_XDECREF(node->c_array[len]);
15161513
}
15171514
}
15181515
Py_TYPE(self)->tp_free(self);
1519-
Py_TRASHCAN_END
15201516
}
15211517

15221518
#ifdef Py_DEBUG
@@ -1878,13 +1874,11 @@ hamt_node_array_dealloc(PyObject *self)
18781874
{
18791875
/* Array's tp_dealloc */
18801876
PyObject_GC_UnTrack(self);
1881-
Py_TRASHCAN_BEGIN(self, hamt_node_array_dealloc)
18821877
PyHamtNode_Array *obj = _PyHamtNode_Array_CAST(self);
18831878
for (Py_ssize_t i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
18841879
Py_XDECREF(obj->a_array[i]);
18851880
}
18861881
Py_TYPE(self)->tp_free(self);
1887-
Py_TRASHCAN_END
18881882
}
18891883

18901884
#ifdef Py_DEBUG

0 commit comments

Comments
 (0)