-
-
Notifications
You must be signed in to change notification settings - Fork 34.2k
gh-139103: fix free-threading dataclass.__init__ perf issue
#141596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
fbda041
273d2af
724897b
e915039
65c695f
aaaec25
c02223e
34d4ffa
3846637
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to hold the type lock here, since
PyUnstable_Object_EnableDeferredRefcountis thread-safe.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ZeroIntensity ,
Thanks very much for pointing this out. ❤
I go through the code of
PyUnstable_Object_EnableDeferredRefcountand find that it's thread-safe.cpython/Objects/object.c
Lines 2734 to 2739 in 3d14805
It uses compare-and-set to guard the
_PyGC_BITS_DEFERREDbit flag before updating the payload of the deferred refcount. So only one thread could make this change.I removed the
TYPE_LOCKand the double check of the_PyObject_HasDeferredRefcount. They are unnecessary.Best Regards,
Edward