Skip to content

Commit 4ad8f09

Browse files
authored
gh-117376: Partial implementation of deferred reference counting (#117696)
This marks objects as using deferred refrence counting using the `ob_gc_bits` field in the free-threaded build and collects those objects during GC.
1 parent c50cb6d commit 4ad8f09

File tree

9 files changed

+82
-21
lines changed

9 files changed

+82
-21
lines changed

Include/internal/pycore_gc.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ static inline PyObject* _Py_FROM_GC(PyGC_Head *gc) {
3939

4040
/* Bit flags for ob_gc_bits (in Py_GIL_DISABLED builds) */
4141
#ifdef Py_GIL_DISABLED
42-
# define _PyGC_BITS_TRACKED (1)
43-
# define _PyGC_BITS_FINALIZED (2)
42+
# define _PyGC_BITS_TRACKED (1) // Tracked by the GC
43+
# define _PyGC_BITS_FINALIZED (2) // tp_finalize was called
4444
# define _PyGC_BITS_UNREACHABLE (4)
4545
# define _PyGC_BITS_FROZEN (8)
4646
# define _PyGC_BITS_SHARED (16)
4747
# define _PyGC_BITS_SHARED_INLINE (32)
48+
# define _PyGC_BITS_DEFERRED (64) // Use deferred reference counting
4849
#endif
4950

5051
/* True if the object is currently tracked by the GC. */

Include/internal/pycore_object.h

+15
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@ static inline void _Py_ClearImmortal(PyObject *op)
158158
op = NULL; \
159159
} while (0)
160160

161+
// Mark an object as supporting deferred reference counting. This is a no-op
162+
// in the default (with GIL) build. Objects that use deferred reference
163+
// counting should be tracked by the GC so that they are eventually collected.
164+
extern void _PyObject_SetDeferredRefcount(PyObject *op);
165+
166+
static inline int
167+
_PyObject_HasDeferredRefcount(PyObject *op)
168+
{
169+
#ifdef Py_GIL_DISABLED
170+
return (op->ob_gc_bits & _PyGC_BITS_DEFERRED) != 0;
171+
#else
172+
return 0;
173+
#endif
174+
}
175+
161176
#if !defined(Py_GIL_DISABLED)
162177
static inline void
163178
_Py_DECREF_SPECIALIZED(PyObject *op, const destructor destruct)

Lib/test/test_code.py

+2
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,7 @@ def test_free_called(self):
834834

835835
SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(100))
836836
del f
837+
gc_collect() # For free-threaded build
837838
self.assertEqual(LAST_FREED, 100)
838839

839840
def test_get_set(self):
@@ -872,6 +873,7 @@ def run(self):
872873
del f
873874
tt.start()
874875
tt.join()
876+
gc_collect() # For free-threaded build
875877
self.assertEqual(LAST_FREED, 500)
876878

877879

Objects/descrobject.c

+1
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@ descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
909909

910910
descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);
911911
if (descr != NULL) {
912+
_PyObject_SetDeferredRefcount((PyObject *)descr);
912913
descr->d_type = (PyTypeObject*)Py_XNewRef(type);
913914
descr->d_name = PyUnicode_InternFromString(name);
914915
if (descr->d_name == NULL) {

Objects/funcobject.c

+9
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
127127
op->func_typeparams = NULL;
128128
op->vectorcall = _PyFunction_Vectorcall;
129129
op->func_version = 0;
130+
// NOTE: functions created via FrameConstructor do not use deferred
131+
// reference counting because they are typically not part of cycles
132+
// nor accessed by multiple threads.
130133
_PyObject_GC_TRACK(op);
131134
handle_func_event(PyFunction_EVENT_CREATE, op, NULL);
132135
return op;
@@ -202,6 +205,12 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
202205
op->func_typeparams = NULL;
203206
op->vectorcall = _PyFunction_Vectorcall;
204207
op->func_version = 0;
208+
if ((code_obj->co_flags & CO_NESTED) == 0) {
209+
// Use deferred reference counting for top-level functions, but not
210+
// nested functions because they are more likely to capture variables,
211+
// which makes prompt deallocation more important.
212+
_PyObject_SetDeferredRefcount((PyObject *)op);
213+
}
205214
_PyObject_GC_TRACK(op);
206215
handle_func_event(PyFunction_EVENT_CREATE, op, NULL);
207216
return (PyObject *)op;

Objects/moduleobject.c

+19-18
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,31 @@ new_module_notrack(PyTypeObject *mt)
8888
m->md_weaklist = NULL;
8989
m->md_name = NULL;
9090
m->md_dict = PyDict_New();
91-
if (m->md_dict != NULL) {
92-
return m;
91+
if (m->md_dict == NULL) {
92+
Py_DECREF(m);
93+
return NULL;
9394
}
94-
Py_DECREF(m);
95-
return NULL;
95+
return m;
96+
}
97+
98+
static void
99+
track_module(PyModuleObject *m)
100+
{
101+
_PyObject_SetDeferredRefcount(m->md_dict);
102+
PyObject_GC_Track(m->md_dict);
103+
104+
_PyObject_SetDeferredRefcount((PyObject *)m);
105+
PyObject_GC_Track(m);
96106
}
97107

98108
static PyObject *
99109
new_module(PyTypeObject *mt, PyObject *args, PyObject *kws)
100110
{
101-
PyObject *m = (PyObject *)new_module_notrack(mt);
111+
PyModuleObject *m = new_module_notrack(mt);
102112
if (m != NULL) {
103-
PyObject_GC_Track(m);
113+
track_module(m);
104114
}
105-
return m;
115+
return (PyObject *)m;
106116
}
107117

108118
PyObject *
@@ -113,7 +123,7 @@ PyModule_NewObject(PyObject *name)
113123
return NULL;
114124
if (module_init_dict(m, m->md_dict, name, NULL) != 0)
115125
goto fail;
116-
PyObject_GC_Track(m);
126+
track_module(m);
117127
return (PyObject *)m;
118128

119129
fail:
@@ -705,16 +715,7 @@ static int
705715
module___init___impl(PyModuleObject *self, PyObject *name, PyObject *doc)
706716
/*[clinic end generated code: output=e7e721c26ce7aad7 input=57f9e177401e5e1e]*/
707717
{
708-
PyObject *dict = self->md_dict;
709-
if (dict == NULL) {
710-
dict = PyDict_New();
711-
if (dict == NULL)
712-
return -1;
713-
self->md_dict = dict;
714-
}
715-
if (module_init_dict(self, dict, name, doc) < 0)
716-
return -1;
717-
return 0;
718+
return module_init_dict(self, self->md_dict, name, doc);
718719
}
719720

720721
static void

Objects/object.c

+13
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,19 @@ _Py_SetImmortal(PyObject *op)
24242424
_Py_SetImmortalUntracked(op);
24252425
}
24262426

2427+
void
2428+
_PyObject_SetDeferredRefcount(PyObject *op)
2429+
{
2430+
#ifdef Py_GIL_DISABLED
2431+
assert(PyType_IS_GC(Py_TYPE(op)));
2432+
assert(_Py_IsOwnedByCurrentThread(op));
2433+
assert(op->ob_ref_shared == 0);
2434+
op->ob_gc_bits |= _PyGC_BITS_DEFERRED;
2435+
op->ob_ref_local += 1;
2436+
op->ob_ref_shared = _Py_REF_QUEUED;
2437+
#endif
2438+
}
2439+
24272440
void
24282441
_Py_ResurrectReference(PyObject *op)
24292442
{

Objects/typeobject.c

+2
Original file line numberDiff line numberDiff line change
@@ -3581,6 +3581,8 @@ type_new_alloc(type_new_ctx *ctx)
35813581
et->ht_module = NULL;
35823582
et->_ht_tpname = NULL;
35833583

3584+
_PyObject_SetDeferredRefcount((PyObject *)et);
3585+
35843586
return type;
35853587
}
35863588

Python/gc_free_threading.c

+18-1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ gc_decref(PyObject *op)
159159
op->ob_tid -= 1;
160160
}
161161

162+
static void
163+
disable_deferred_refcounting(PyObject *op)
164+
{
165+
if (_PyObject_HasDeferredRefcount(op)) {
166+
op->ob_gc_bits &= ~_PyGC_BITS_DEFERRED;
167+
op->ob_ref_shared -= (1 << _Py_REF_SHARED_SHIFT);
168+
}
169+
}
170+
162171
static Py_ssize_t
163172
merge_refcount(PyObject *op, Py_ssize_t extra)
164173
{
@@ -375,9 +384,10 @@ update_refs(const mi_heap_t *heap, const mi_heap_area_t *area,
375384
}
376385

377386
Py_ssize_t refcount = Py_REFCNT(op);
387+
refcount -= _PyObject_HasDeferredRefcount(op);
378388
_PyObject_ASSERT(op, refcount >= 0);
379389

380-
if (refcount > 0) {
390+
if (refcount > 0 && !_PyObject_HasDeferredRefcount(op)) {
381391
// Untrack tuples and dicts as necessary in this pass, but not objects
382392
// with zero refcount, which we will want to collect.
383393
if (PyTuple_CheckExact(op)) {
@@ -466,6 +476,9 @@ mark_heap_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
466476
return true;
467477
}
468478

479+
_PyObject_ASSERT_WITH_MSG(op, gc_get_refs(op) >= 0,
480+
"refcount is too small");
481+
469482
if (gc_is_unreachable(op) && gc_get_refs(op) != 0) {
470483
// Object is reachable but currently marked as unreachable.
471484
// Mark it as reachable and traverse its pointers to find
@@ -499,6 +512,10 @@ scan_heap_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
499512

500513
struct collection_state *state = (struct collection_state *)args;
501514
if (gc_is_unreachable(op)) {
515+
// Disable deferred refcounting for unreachable objects so that they
516+
// are collected immediately after finalization.
517+
disable_deferred_refcounting(op);
518+
502519
// Merge and add one to the refcount to prevent deallocation while we
503520
// are holding on to it in a worklist.
504521
merge_refcount(op, 1);

0 commit comments

Comments
 (0)