From fbda041c8f87152010d1f6aa364511c7f7d8b5bb Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 15 Nov 2025 20:44:56 +0800 Subject: [PATCH 1/8] make lock with fine-grianed --- Objects/typeobject.c | 13 ++++++ edward-test-bench.py | 94 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 edward-test-bench.py diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 61bcc21ce13d47..3b5054c4bb803b 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6546,6 +6546,18 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) assert(!_PyType_HasFeature(metatype, Py_TPFLAGS_INLINE_VALUES)); assert(!_PyType_HasFeature(metatype, Py_TPFLAGS_MANAGED_DICT)); +#ifdef Py_GIL_DISABLED + if (value != NULL && PyFunction_Check(value)) { + if (!_PyObject_HasDeferredRefcount(value)) { + BEGIN_TYPE_LOCK(); + if (!_PyObject_HasDeferredRefcount(value)) { + PyUnstable_Object_EnableDeferredRefcount(value); + } + END_TYPE_LOCK(); + } + } +#endif + PyObject *old_value = NULL; PyObject *descr = _PyType_LookupRef(metatype, name); if (descr != NULL) { @@ -6573,6 +6585,7 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) } } + BEGIN_TYPE_DICT_LOCK(dict); res = type_update_dict(type, (PyDictObject *)dict, name, value, &old_value); assert(_PyType_CheckConsistency(type)); diff --git a/edward-test-bench.py b/edward-test-bench.py new file mode 100644 index 00000000000000..c3e95fa8b10be8 --- /dev/null +++ b/edward-test-bench.py @@ -0,0 +1,94 @@ +import _testcapi +from threading import Thread +from time import time + +def test_1(): + class Foo: + def __init__(self, x): + self.x = x + + niter = 5 * 1000 * 1000 + + def benchmark(n): + for i in range(n): + Foo(x=1) + + for nth in (1, 4): + t0 = time() + threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)] + for t in threads: + t.start() + for t in threads: + t.join() + print(f"{nth=} {(time() - t0) / nth}") + +def test_2(): + class Foo2: + def __init__(self, x): + pass + pass + + _Foo2_x = int + + create_str = """def create_init(_Foo2_x,): + def __init__(self, x: _Foo2_x): + self.x = x + return (__init__,) + """ + ns = {} + exec(create_str, globals(), ns) + fn = ns['create_init']({**locals()}) + setattr(Foo2, '__init__', fn[0]) + niter = 5 * 1000 * 1000 + def benchmark(n): + for i in range(n): + Foo2(x=1) + + for nth in (1, 4): + t0 = time() + threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)] + for t in threads: + t.start() + for t in threads: + t.join() + print(f"{nth=} {(time() - t0) / nth}") + +def test_3(): + class Foo3: + def __init__(self, x): + pass + pass + + _Foo3_x = int + + create_str = """def create_init(_Foo3_x,): + def __init__(self, x: _Foo3_x): + self.x = x + return (__init__,) + """ + ns = {} + exec(create_str, globals(), ns) + fn = ns['create_init']({**locals()}) + setattr(Foo3, '__init__', fn[0]) + _testcapi.pyobject_enable_deferred_refcount(Foo3.__init__) + niter = 5 * 1000 * 1000 + def benchmark(n): + for i in range(n): + Foo3(x=1) + + for nth in (1, 4): + t0 = time() + threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)] + for t in threads: + t.start() + for t in threads: + t.join() + print(f"{nth=} {(time() - t0) / nth}") + +if __name__ == "__main__": + print("------test_1-------") + test_1() + print("------test_2-------") + test_2() + print("------test_3-------") + test_3() \ No newline at end of file From 273d2afd2b5d5842ff5fa1398e0aadd9b4d575e0 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 15 Nov 2025 23:36:30 +0800 Subject: [PATCH 2/8] add benchmark script --- Tools/ftscalingbench/ftscalingbench.py | 12 ++++ edward-test-bench.py | 94 -------------------------- 2 files changed, 12 insertions(+), 94 deletions(-) delete mode 100644 edward-test-bench.py diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index 1a59e25189d5dd..e8e7006df663f7 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -27,6 +27,7 @@ import sys import threading import time +from dataclasses import dataclass from operator import methodcaller # The iterations in individual benchmarks are scaled by this factor. @@ -201,6 +202,17 @@ def method_caller(): obj = MyClass() for i in range(1000 * WORK_SCALE): mc(obj) + +@dataclass +class MyDataClass: + x: int + y: int + z: int + +@register_benchmark +def instantiate_dataclass(): + for _ in range(1000 * WORK_SCALE): + obj = MyDataClass(x=1, y=2, z=3) def bench_one_thread(func): t0 = time.perf_counter_ns() diff --git a/edward-test-bench.py b/edward-test-bench.py deleted file mode 100644 index c3e95fa8b10be8..00000000000000 --- a/edward-test-bench.py +++ /dev/null @@ -1,94 +0,0 @@ -import _testcapi -from threading import Thread -from time import time - -def test_1(): - class Foo: - def __init__(self, x): - self.x = x - - niter = 5 * 1000 * 1000 - - def benchmark(n): - for i in range(n): - Foo(x=1) - - for nth in (1, 4): - t0 = time() - threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)] - for t in threads: - t.start() - for t in threads: - t.join() - print(f"{nth=} {(time() - t0) / nth}") - -def test_2(): - class Foo2: - def __init__(self, x): - pass - pass - - _Foo2_x = int - - create_str = """def create_init(_Foo2_x,): - def __init__(self, x: _Foo2_x): - self.x = x - return (__init__,) - """ - ns = {} - exec(create_str, globals(), ns) - fn = ns['create_init']({**locals()}) - setattr(Foo2, '__init__', fn[0]) - niter = 5 * 1000 * 1000 - def benchmark(n): - for i in range(n): - Foo2(x=1) - - for nth in (1, 4): - t0 = time() - threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)] - for t in threads: - t.start() - for t in threads: - t.join() - print(f"{nth=} {(time() - t0) / nth}") - -def test_3(): - class Foo3: - def __init__(self, x): - pass - pass - - _Foo3_x = int - - create_str = """def create_init(_Foo3_x,): - def __init__(self, x: _Foo3_x): - self.x = x - return (__init__,) - """ - ns = {} - exec(create_str, globals(), ns) - fn = ns['create_init']({**locals()}) - setattr(Foo3, '__init__', fn[0]) - _testcapi.pyobject_enable_deferred_refcount(Foo3.__init__) - niter = 5 * 1000 * 1000 - def benchmark(n): - for i in range(n): - Foo3(x=1) - - for nth in (1, 4): - t0 = time() - threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)] - for t in threads: - t.start() - for t in threads: - t.join() - print(f"{nth=} {(time() - t0) / nth}") - -if __name__ == "__main__": - print("------test_1-------") - test_1() - print("------test_2-------") - test_2() - print("------test_3-------") - test_3() \ No newline at end of file From 724897baa1e5942396aa1adcc5b8c0c47285c391 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 16 Nov 2025 00:00:54 +0800 Subject: [PATCH 3/8] add news entry. --- .../2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst new file mode 100644 index 00000000000000..8208826c5ee3f8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst @@ -0,0 +1 @@ +Make :func:`type_setattro` use defer refcount in free-threading for functions without defer refcount. From e91503918074d5a2f2a8b9578bc64002205f89f6 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 16 Nov 2025 00:13:30 +0800 Subject: [PATCH 4/8] fix for pre-commit. --- Objects/typeobject.c | 2 +- Tools/ftscalingbench/ftscalingbench.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 3b5054c4bb803b..6787e68f2e2eed 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6585,7 +6585,7 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) } } - + BEGIN_TYPE_DICT_LOCK(dict); res = type_update_dict(type, (PyDictObject *)dict, name, value, &old_value); assert(_PyType_CheckConsistency(type)); diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index e8e7006df663f7..097a065f368f30 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -202,7 +202,7 @@ def method_caller(): obj = MyClass() for i in range(1000 * WORK_SCALE): mc(obj) - + @dataclass class MyDataClass: x: int From 65c695fe992fcee890670568e13c3465e8d3887a Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 16 Nov 2025 00:16:08 +0800 Subject: [PATCH 5/8] fix news for docs check. --- .../2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst index 8208826c5ee3f8..fbb11c72cb3994 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst @@ -1 +1 @@ -Make :func:`type_setattro` use defer refcount in free-threading for functions without defer refcount. +Make :c:func:`type_setattro` use defer refcount in free-threading for functions without defer refcount. From aaaec255ff8c41f6f021ad5ad911f2632e21013d Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 16 Nov 2025 13:38:37 +0800 Subject: [PATCH 6/8] update news entry --- .../2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst index fbb11c72cb3994..d6710f88ed105a 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst @@ -1 +1 @@ -Make :c:func:`type_setattro` use defer refcount in free-threading for functions without defer refcount. +Make ``type_setattro`` use defer refcount in free-threading for functions without defer refcount. From c02223e6a24f5b64f839af3e21b1df73bd711c6d Mon Sep 17 00:00:00 2001 From: edward_xu Date: Mon, 17 Nov 2025 21:56:50 +0800 Subject: [PATCH 7/8] remove redundant lock --- .../2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst | 2 +- Objects/typeobject.c | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst index d6710f88ed105a..c038dc742ccec9 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-15-23-58-23.gh-issue-139103.9cVYJ0.rst @@ -1 +1 @@ -Make ``type_setattro`` use defer refcount in free-threading for functions without defer refcount. +Improve multithreaded scaling of dataclasses on the free-threaded build. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 6787e68f2e2eed..3ff9be49124bb4 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6547,14 +6547,10 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) assert(!_PyType_HasFeature(metatype, Py_TPFLAGS_MANAGED_DICT)); #ifdef Py_GIL_DISABLED - if (value != NULL && PyFunction_Check(value)) { - if (!_PyObject_HasDeferredRefcount(value)) { - BEGIN_TYPE_LOCK(); - if (!_PyObject_HasDeferredRefcount(value)) { - PyUnstable_Object_EnableDeferredRefcount(value); - } - END_TYPE_LOCK(); - } + if (value != NULL && + PyFunction_Check(value) && + !_PyObject_HasDeferredRefcount(value)) { + PyUnstable_Object_EnableDeferredRefcount(value); } #endif @@ -6585,7 +6581,6 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) } } - BEGIN_TYPE_DICT_LOCK(dict); res = type_update_dict(type, (PyDictObject *)dict, name, value, &old_value); assert(_PyType_CheckConsistency(type)); From 34d4ffa59b55489b0b49bb66e7bd2800fb19a450 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 18 Nov 2025 18:05:11 -0500 Subject: [PATCH 8/8] Format if statement and add comment --- Objects/typeobject.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 3ff9be49124bb4..c99c6b3f6377b6 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6547,10 +6547,14 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) assert(!_PyType_HasFeature(metatype, Py_TPFLAGS_MANAGED_DICT)); #ifdef Py_GIL_DISABLED + // gh-139103: Enable deferred refcounting for functions assigned + // to type objects. This is important for `dataclass.__init__`, + // which is generated dynamically. if (value != NULL && PyFunction_Check(value) && - !_PyObject_HasDeferredRefcount(value)) { - PyUnstable_Object_EnableDeferredRefcount(value); + !_PyObject_HasDeferredRefcount(value)) + { + PyUnstable_Object_EnableDeferredRefcount(value); } #endif