diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index 7946550ec0db63..59f23b86810ce0 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -5,9 +5,11 @@ import threading import types import unittest +import weakref from test.support import (threading_helper, check_impl_detail, - requires_specialization, - cpython_only, requires_jit_disabled, reset_code) + requires_specialization, Py_GIL_DISABLED, + cpython_only, requires_jit_disabled, reset_code, + gc_collect) from test.support.import_helper import import_module # Skip this module on other interpreters, it is cpython specific: @@ -2240,6 +2242,38 @@ def load_enum_member(): self.assert_specialized(load_enum_member, "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK") + @cpython_only + @requires_specialization + @unittest.skipUnless(Py_GIL_DISABLED, + "deferred refcounting needs the free-threaded build") + @threading_helper.requires_working_threading() + def test_load_attr_class_mutable_value_deferred_non_owning_threading(self): + # Ensure load attributes from class mutable values + # are still enabled for deferred referencing + # counting from a non owning thread + class Value: + pass + + class Holder: + attr = Value() + + value = Holder.attr + wr = weakref.ref(value) + + def load_class_attr(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + x = Holder.attr + assert x is value + + # Ensure we specialise from a non owning thread + threading_helper.run_concurrently(load_class_attr, nthreads=1) + self.assert_no_opcode(load_class_attr, "LOAD_ATTR_CLASS") + self.assertTrue(_testinternalcapi.has_deferred_refcount(value)) + + del load_class_attr, Holder, value + gc_collect() + self.assertIsNone(wr()) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-15-23-32-52.gh-issue-139103.d8pm7q.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-15-23-32-52.gh-issue-139103.d8pm7q.rst new file mode 100644 index 00000000000000..0fd62c333c42e3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-15-23-32-52.gh-issue-139103.d8pm7q.rst @@ -0,0 +1 @@ +Enable deferred reference counting for mutable class attributes to improve free-threading scaling diff --git a/Python/specialize.c b/Python/specialize.c index 773b55c329c772..2695fcb85edc00 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -1216,6 +1216,9 @@ specialize_class_load_attr(PyObject *owner, _Py_CODEUNIT *instr, // special case for enums which has Py_TYPE(descr) == cls // so guarding on type version is sufficient if (Py_TYPE(descr) != cls) { +#ifdef Py_GIL_DISABLED + maybe_enable_deferred_ref_count(descr); +#endif SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_MUTABLE_CLASS); Py_XDECREF(descr); return -1; diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index c8a914c22a9e13..609d71ed19df5c 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -326,6 +326,25 @@ def enum_attr(): MyEnum.Z +class _SharedAttrValue: + pass + +class MyClassWithSharedAttr: + # A class attribute whose value is an instance of a *different* class. + # Reading it from multiple threads did not scale because LOAD_ATTR_CLASS + # cannot specialize this case (the value's type is not the owner class), + # leaving the shared value's reference count contended on every read. + attr = _SharedAttrValue() + +@register_benchmark +def class_attribute(): + obj = MyClassWithSharedAttr + for _ in range(1000 * WORK_SCALE): + obj.attr + obj.attr + obj.attr + + def bench_one_thread(func): t0 = time.perf_counter_ns() func()