]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-117376: Partial implementation of deferred reference counting (#117696)
authorSam Gross <colesbury@gmail.com>
Fri, 12 Apr 2024 17:36:20 +0000 (13:36 -0400)
committerGitHub <noreply@github.com>
Fri, 12 Apr 2024 17:36:20 +0000 (17:36 +0000)
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.

Include/internal/pycore_gc.h
Include/internal/pycore_object.h
Lib/test/test_code.py
Objects/descrobject.c
Objects/funcobject.c
Objects/moduleobject.c
Objects/object.c
Objects/typeobject.c
Python/gc_free_threading.c

index c4482c4ffcfa60ce83d381753ee2da5fca5d6721..60020b5c01f8a6c51665a9527af022335c58e98d 100644 (file)
@@ -39,12 +39,13 @@ static inline PyObject* _Py_FROM_GC(PyGC_Head *gc) {
 
 /* Bit flags for ob_gc_bits (in Py_GIL_DISABLED builds) */
 #ifdef Py_GIL_DISABLED
-#  define _PyGC_BITS_TRACKED        (1)
-#  define _PyGC_BITS_FINALIZED      (2)
+#  define _PyGC_BITS_TRACKED        (1)     // Tracked by the GC
+#  define _PyGC_BITS_FINALIZED      (2)     // tp_finalize was called
 #  define _PyGC_BITS_UNREACHABLE    (4)
 #  define _PyGC_BITS_FROZEN         (8)
 #  define _PyGC_BITS_SHARED         (16)
 #  define _PyGC_BITS_SHARED_INLINE  (32)
+#  define _PyGC_BITS_DEFERRED       (64)    // Use deferred reference counting
 #endif
 
 /* True if the object is currently tracked by the GC. */
index 9aa2e5bf918a7b92e2de22749c1e5bd0bbf19e6b..7b1c919e627dd4f8f4f1b7343aec31f7d3e96855 100644 (file)
@@ -158,6 +158,21 @@ static inline void _Py_ClearImmortal(PyObject *op)
         op = NULL; \
     } while (0)
 
+// Mark an object as supporting deferred reference counting. This is a no-op
+// in the default (with GIL) build. Objects that use deferred reference
+// counting should be tracked by the GC so that they are eventually collected.
+extern void _PyObject_SetDeferredRefcount(PyObject *op);
+
+static inline int
+_PyObject_HasDeferredRefcount(PyObject *op)
+{
+#ifdef Py_GIL_DISABLED
+    return (op->ob_gc_bits & _PyGC_BITS_DEFERRED) != 0;
+#else
+    return 0;
+#endif
+}
+
 #if !defined(Py_GIL_DISABLED)
 static inline void
 _Py_DECREF_SPECIALIZED(PyObject *op, const destructor destruct)
index ecd1e82a6dbef9e22af5798cab6968bb7303b44a..5c0fdc8edc31b66944424bfcec96bde583d86217 100644 (file)
@@ -834,6 +834,7 @@ if check_impl_detail(cpython=True) and ctypes is not None:
 
             SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(100))
             del f
+            gc_collect()  # For free-threaded build
             self.assertEqual(LAST_FREED, 100)
 
         def test_get_set(self):
@@ -872,6 +873,7 @@ if check_impl_detail(cpython=True) and ctypes is not None:
             del f
             tt.start()
             tt.join()
+            gc_collect()  # For free-threaded build
             self.assertEqual(LAST_FREED, 500)
 
 
index 029318faf0acd2392e9e1b2dad5973dc0bb3c084..1b7e2fde3ceccd703929cdf3bca39fa2e0323bbb 100644 (file)
@@ -909,6 +909,7 @@ descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
 
     descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);
     if (descr != NULL) {
+        _PyObject_SetDeferredRefcount((PyObject *)descr);
         descr->d_type = (PyTypeObject*)Py_XNewRef(type);
         descr->d_name = PyUnicode_InternFromString(name);
         if (descr->d_name == NULL) {
index a3c0800e7891d38a790d8fa3f1711d251f49e10b..276b3db29703711e8f1b0a62c0e18402fe9b7a97 100644 (file)
@@ -127,6 +127,9 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
     op->func_typeparams = NULL;
     op->vectorcall = _PyFunction_Vectorcall;
     op->func_version = 0;
+    // NOTE: functions created via FrameConstructor do not use deferred
+    // reference counting because they are typically not part of cycles
+    // nor accessed by multiple threads.
     _PyObject_GC_TRACK(op);
     handle_func_event(PyFunction_EVENT_CREATE, op, NULL);
     return op;
@@ -202,6 +205,12 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
     op->func_typeparams = NULL;
     op->vectorcall = _PyFunction_Vectorcall;
     op->func_version = 0;
+    if ((code_obj->co_flags & CO_NESTED) == 0) {
+        // Use deferred reference counting for top-level functions, but not
+        // nested functions because they are more likely to capture variables,
+        // which makes prompt deallocation more important.
+        _PyObject_SetDeferredRefcount((PyObject *)op);
+    }
     _PyObject_GC_TRACK(op);
     handle_func_event(PyFunction_EVENT_CREATE, op, NULL);
     return (PyObject *)op;
index 9cd98fb4345fdd6beb812add89afe3841023a970..da6a276c41be1fe8bb940ed54f29ed08cc7daeb3 100644 (file)
@@ -88,21 +88,31 @@ new_module_notrack(PyTypeObject *mt)
     m->md_weaklist = NULL;
     m->md_name = NULL;
     m->md_dict = PyDict_New();
-    if (m->md_dict != NULL) {
-        return m;
+    if (m->md_dict == NULL) {
+        Py_DECREF(m);
+        return NULL;
     }
-    Py_DECREF(m);
-    return NULL;
+    return m;
+}
+
+static void
+track_module(PyModuleObject *m)
+{
+    _PyObject_SetDeferredRefcount(m->md_dict);
+    PyObject_GC_Track(m->md_dict);
+
+    _PyObject_SetDeferredRefcount((PyObject *)m);
+    PyObject_GC_Track(m);
 }
 
 static PyObject *
 new_module(PyTypeObject *mt, PyObject *args, PyObject *kws)
 {
-    PyObject *m = (PyObject *)new_module_notrack(mt);
+    PyModuleObject *m = new_module_notrack(mt);
     if (m != NULL) {
-        PyObject_GC_Track(m);
+        track_module(m);
     }
-    return m;
+    return (PyObject *)m;
 }
 
 PyObject *
@@ -113,7 +123,7 @@ PyModule_NewObject(PyObject *name)
         return NULL;
     if (module_init_dict(m, m->md_dict, name, NULL) != 0)
         goto fail;
-    PyObject_GC_Track(m);
+    track_module(m);
     return (PyObject *)m;
 
  fail:
@@ -705,16 +715,7 @@ static int
 module___init___impl(PyModuleObject *self, PyObject *name, PyObject *doc)
 /*[clinic end generated code: output=e7e721c26ce7aad7 input=57f9e177401e5e1e]*/
 {
-    PyObject *dict = self->md_dict;
-    if (dict == NULL) {
-        dict = PyDict_New();
-        if (dict == NULL)
-            return -1;
-        self->md_dict = dict;
-    }
-    if (module_init_dict(self, dict, name, doc) < 0)
-        return -1;
-    return 0;
+    return module_init_dict(self, self->md_dict, name, doc);
 }
 
 static void
index 17f75b43e82d3ada2e5e4996369cd48b53e6ef1e..016d0e1ded92d86d601929d0ab75ed5a26197cf7 100644 (file)
@@ -2424,6 +2424,19 @@ _Py_SetImmortal(PyObject *op)
     _Py_SetImmortalUntracked(op);
 }
 
+void
+_PyObject_SetDeferredRefcount(PyObject *op)
+{
+#ifdef Py_GIL_DISABLED
+    assert(PyType_IS_GC(Py_TYPE(op)));
+    assert(_Py_IsOwnedByCurrentThread(op));
+    assert(op->ob_ref_shared == 0);
+    op->ob_gc_bits |= _PyGC_BITS_DEFERRED;
+    op->ob_ref_local += 1;
+    op->ob_ref_shared = _Py_REF_QUEUED;
+#endif
+}
+
 void
 _Py_ResurrectReference(PyObject *op)
 {
index ee0286e4009ec44025b4f41a96250049dfb0ced7..a3c137536a4d870beca6decd73d2ea7ccebb5cd2 100644 (file)
@@ -3581,6 +3581,8 @@ type_new_alloc(type_new_ctx *ctx)
     et->ht_module = NULL;
     et->_ht_tpname = NULL;
 
+    _PyObject_SetDeferredRefcount((PyObject *)et);
+
     return type;
 }
 
index 111632ffb7764109e0488a17ea4539cf74a1afdf..9cf0e989d0993f5bf488b3e92486a37063ef614e 100644 (file)
@@ -159,6 +159,15 @@ gc_decref(PyObject *op)
     op->ob_tid -= 1;
 }
 
+static void
+disable_deferred_refcounting(PyObject *op)
+{
+    if (_PyObject_HasDeferredRefcount(op)) {
+        op->ob_gc_bits &= ~_PyGC_BITS_DEFERRED;
+        op->ob_ref_shared -= (1 << _Py_REF_SHARED_SHIFT);
+    }
+}
+
 static Py_ssize_t
 merge_refcount(PyObject *op, Py_ssize_t extra)
 {
@@ -375,9 +384,10 @@ update_refs(const mi_heap_t *heap, const mi_heap_area_t *area,
     }
 
     Py_ssize_t refcount = Py_REFCNT(op);
+    refcount -= _PyObject_HasDeferredRefcount(op);
     _PyObject_ASSERT(op, refcount >= 0);
 
-    if (refcount > 0) {
+    if (refcount > 0 && !_PyObject_HasDeferredRefcount(op)) {
         // Untrack tuples and dicts as necessary in this pass, but not objects
         // with zero refcount, which we will want to collect.
         if (PyTuple_CheckExact(op)) {
@@ -466,6 +476,9 @@ mark_heap_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
         return true;
     }
 
+    _PyObject_ASSERT_WITH_MSG(op, gc_get_refs(op) >= 0,
+                                  "refcount is too small");
+
     if (gc_is_unreachable(op) && gc_get_refs(op) != 0) {
         // Object is reachable but currently marked as unreachable.
         // 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,
 
     struct collection_state *state = (struct collection_state *)args;
     if (gc_is_unreachable(op)) {
+        // Disable deferred refcounting for unreachable objects so that they
+        // are collected immediately after finalization.
+        disable_deferred_refcounting(op);
+
         // Merge and add one to the refcount to prevent deallocation while we
         // are holding on to it in a worklist.
         merge_refcount(op, 1);