]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-133164: Add `PyUnstable_Object_IsUniqueReferencedTemporary` C API (gh-133170)
authorSam Gross <colesbury@gmail.com>
Fri, 2 May 2025 13:24:57 +0000 (09:24 -0400)
committerGitHub <noreply@github.com>
Fri, 2 May 2025 13:24:57 +0000 (13:24 +0000)
After gh-130704, the interpreter replaces some uses of `LOAD_FAST` with
`LOAD_FAST_BORROW` which avoid incref/decrefs by "borrowing" references
on the interpreter stack when the bytecode compiler can determine that
it's safe.

This change broke some checks in C API extensions that relied on
`Py_REFCNT()` of `1` to determine if it's safe to modify an object
in-place. Objects may have a reference count of one, but still be
referenced further up the interpreter stack due to borrowing of
references.

This provides a replacement function for those checks.
`PyUnstable_Object_IsUniqueReferencedTemporary` is more conservative:
it checks that the object has a reference count of one and that it exists as a
unique strong reference in the interpreter's stack of temporary
variables in the top most frame.

See also:

* https://github.com/numpy/numpy/issues/28681

Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
Co-authored-by: T. Wouters <thomas@python.org>
Co-authored-by: mpage <mpage@cs.stanford.edu>
Co-authored-by: Mark Shannon <mark@hotpy.org>
Co-authored-by: Victor Stinner <vstinner@python.org>
Doc/c-api/object.rst
Doc/c-api/refcounting.rst
Doc/whatsnew/3.14.rst
Include/cpython/object.h
Lib/test/test_capi/test_object.py
Misc/NEWS.d/next/C_API/2025-04-29-19-39-16.gh-issue-133164.W-XTU7.rst [new file with mode: 0644]
Modules/_testcapi/object.c
Objects/object.c

index bef3a79ccd0e217e76cad1cdb93cc19af8a412ee..efad4d215b19869a6b09cace21fa24246696a6f6 100644 (file)
@@ -613,6 +613,38 @@ Object Protocol
 
    .. versionadded:: 3.14
 
+.. c:function:: int PyUnstable_Object_IsUniqueReferencedTemporary(PyObject *obj)
+
+   Check if *obj* is a unique temporary object.
+   Returns ``1`` if *obj* is known to be a unique temporary object,
+   and ``0`` otherwise.  This function cannot fail, but the check is
+   conservative, and may return ``0`` in some cases even if *obj* is a unique
+   temporary object.
+
+   If an object is a unique temporary, it is guaranteed that the current code
+   has the only reference to the object. For arguments to C functions, this
+   should be used instead of checking if the reference count is ``1``. Starting
+   with Python 3.14, the interpreter internally avoids some reference count
+   modifications when loading objects onto the operands stack by
+   :term:`borrowing <borrowed reference>` references when possible, which means
+   that a reference count of ``1`` by itself does not guarantee that a function
+   argument uniquely referenced.
+
+   In the example below, ``my_func`` is called with a unique temporary object
+   as its argument::
+
+      my_func([1, 2, 3])
+
+   In the example below, ``my_func`` is **not** called with a unique temporary
+   object as its argument, even if its refcount is ``1``::
+
+      my_list = [1, 2, 3]
+      my_func(my_list)
+
+   See also the function :c:func:`Py_REFCNT`.
+
+   .. versionadded:: 3.14
+
 .. c:function:: int PyUnstable_IsImmortal(PyObject *obj)
 
    This function returns non-zero if *obj* is :term:`immortal`, and zero
index d75dad737bc992f9ecef0b047758bba4ce12e030..83febcf70a55487695215b49f482985f5ff3ecbc 100644 (file)
@@ -23,6 +23,8 @@ of Python objects.
 
    Use the :c:func:`Py_SET_REFCNT()` function to set an object reference count.
 
+   See also the function :c:func:`PyUnstable_Object_IsUniqueReferencedTemporary()`.
+
    .. versionchanged:: 3.10
       :c:func:`Py_REFCNT()` is changed to the inline static function.
 
index 0d2e6533d05f8d501cbe0577239cb117b397e6dd..460b77a2385911ced18c5c31f4770da023f112f8 100644 (file)
@@ -89,6 +89,10 @@ If you encounter :exc:`NameError`\s or pickling errors coming out of
 :mod:`multiprocessing` or :mod:`concurrent.futures`, see the
 :ref:`forkserver restrictions <multiprocessing-programming-forkserver>`.
 
+The interpreter avoids some reference count modifications internally when
+it's safe to do so. This can lead to different values returned from
+:func:`sys.getrefcount` and :c:func:`Py_REFCNT` compared to previous versions
+of Python.  See :ref:`below <whatsnew314-refcount>` for details.
 
 New features
 ============
@@ -2215,6 +2219,11 @@ New features
   take a C integer and produce a Python :class:`bool` object. (Contributed by
   Pablo Galindo in :issue:`45325`.)
 
+* Add :c:func:`PyUnstable_Object_IsUniqueReferencedTemporary` to determine if an object
+  is a unique temporary object on the interpreter's operand stack. This can
+  be used in some cases as a replacement for checking if :c:func:`Py_REFCNT`
+  is ``1`` for Python objects passed as arguments to C API functions.
+
 
 Limited C API changes
 ---------------------
@@ -2249,6 +2258,17 @@ Porting to Python 3.14
   a :exc:`UnicodeError` object.
   (Contributed by Bénédikt Tran in :gh:`127691`.)
 
+.. _whatsnew314-refcount:
+
+* The interpreter internally avoids some reference count modifications when
+  loading objects onto the operands stack by :term:`borrowing <borrowed reference>`
+  references when possible. This can lead to smaller reference count values
+  compared to previous Python versions. C API extensions that checked
+  :c:func:`Py_REFCNT` of ``1`` to determine if an function argument is not
+  referenced by any other code should instead use
+  :c:func:`PyUnstable_Object_IsUniqueReferencedTemporary` as a safer replacement.
+
+
 * Private functions promoted to public C APIs:
 
   * ``_PyBytes_Join()``: :c:func:`PyBytes_Join`.
index b6c508e6e29649b728e176cb5cf017d99047d2fb..3a4d65f7712c632fe7ee7838df34ef1a024e2143 100644 (file)
@@ -476,6 +476,11 @@ PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);
  */
 PyAPI_FUNC(int) PyUnstable_Object_EnableDeferredRefcount(PyObject *);
 
+/* Determine if the object exists as a unique temporary variable on the
+ * topmost frame of the interpreter.
+ */
+PyAPI_FUNC(int) PyUnstable_Object_IsUniqueReferencedTemporary(PyObject *);
+
 /* Check whether the object is immortal. This cannot fail. */
 PyAPI_FUNC(int) PyUnstable_IsImmortal(PyObject *);
 
index 3e8fd91b9a67a0eaed4cb0228f9f20f7c7e015f9..54a01ac7c4a7ae7f3f5ad14ad88f2861a66a4733 100644 (file)
@@ -1,4 +1,5 @@
 import enum
+import sys
 import textwrap
 import unittest
 from test import support
@@ -223,5 +224,17 @@ class CAPITest(unittest.TestCase):
             obj = MyObj()
             _testinternalcapi.incref_decref_delayed(obj)
 
+    def test_is_unique_temporary(self):
+        self.assertTrue(_testcapi.pyobject_is_unique_temporary(object()))
+        obj = object()
+        self.assertFalse(_testcapi.pyobject_is_unique_temporary(obj))
+
+        def func(x):
+            # This relies on the LOAD_FAST_BORROW optimization (gh-130704)
+            self.assertEqual(sys.getrefcount(x), 1)
+            self.assertFalse(_testcapi.pyobject_is_unique_temporary(x))
+
+        func(object())
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Misc/NEWS.d/next/C_API/2025-04-29-19-39-16.gh-issue-133164.W-XTU7.rst b/Misc/NEWS.d/next/C_API/2025-04-29-19-39-16.gh-issue-133164.W-XTU7.rst
new file mode 100644 (file)
index 0000000..dec7c76
--- /dev/null
@@ -0,0 +1,5 @@
+Add :c:func:`PyUnstable_Object_IsUniqueReferencedTemporary` function for
+determining if an object exists as a unique temporary variable on the
+interpreter's stack.  This is a replacement for some cases where checking
+that :c:func:`Py_REFCNT` is one is no longer sufficient to determine if it's
+safe to modify a Python object in-place with no visible side effects.
index 2d538627d213fd49b7f2eb6cefa9a27d4d070ef3..5c67adfee29dc1cc72163258900d1f33df8bd678 100644 (file)
@@ -131,6 +131,13 @@ pyobject_enable_deferred_refcount(PyObject *self, PyObject *obj)
     return PyLong_FromLong(result);
 }
 
+static PyObject *
+pyobject_is_unique_temporary(PyObject *self, PyObject *obj)
+{
+    int result = PyUnstable_Object_IsUniqueReferencedTemporary(obj);
+    return PyLong_FromLong(result);
+}
+
 static int MyObject_dealloc_called = 0;
 
 static void
@@ -478,6 +485,7 @@ static PyMethodDef test_methods[] = {
     {"pyobject_print_os_error", pyobject_print_os_error, METH_VARARGS},
     {"pyobject_clear_weakrefs_no_callbacks", pyobject_clear_weakrefs_no_callbacks, METH_O},
     {"pyobject_enable_deferred_refcount", pyobject_enable_deferred_refcount, METH_O},
+    {"pyobject_is_unique_temporary", pyobject_is_unique_temporary, METH_O},
     {"test_py_try_inc_ref", test_py_try_inc_ref, METH_NOARGS},
     {"test_xincref_doesnt_leak",test_xincref_doesnt_leak,        METH_NOARGS},
     {"test_incref_doesnt_leak", test_incref_doesnt_leak,         METH_NOARGS},
index 70d10b071d2d983e7fb5e0ae15a8a845874727fd..0974a231ec101a389a2d8b40b4d4506b3e52ce5e 100644 (file)
@@ -15,6 +15,7 @@
 #include "pycore_hamt.h"          // _PyHamtItems_Type
 #include "pycore_initconfig.h"    // _PyStatus_OK()
 #include "pycore_instruction_sequence.h" // _PyInstructionSequence_Type
+#include "pycore_interpframe.h"   // _PyFrame_Stackbase()
 #include "pycore_interpolation.h" // _PyInterpolation_Type
 #include "pycore_list.h"          // _PyList_DebugMallocStats()
 #include "pycore_long.h"          // _PyLong_GetZero()
@@ -2621,6 +2622,29 @@ PyUnstable_Object_EnableDeferredRefcount(PyObject *op)
 #endif
 }
 
+int
+PyUnstable_Object_IsUniqueReferencedTemporary(PyObject *op)
+{
+    if (!_PyObject_IsUniquelyReferenced(op)) {
+        return 0;
+    }
+
+    _PyInterpreterFrame *frame = _PyEval_GetFrame();
+    if (frame == NULL) {
+        return 0;
+    }
+
+    _PyStackRef *base = _PyFrame_Stackbase(frame);
+    _PyStackRef *stackpointer = frame->stackpointer;
+    while (stackpointer > base) {
+        stackpointer--;
+        if (op == PyStackRef_AsPyObjectBorrow(*stackpointer)) {
+            return PyStackRef_IsHeapSafe(*stackpointer);
+        }
+    }
+    return 0;
+}
+
 int
 PyUnstable_TryIncRef(PyObject *op)
 {