Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
make lock with fine-grianed
  • Loading branch information
LindaSummer committed Nov 16, 2025
commit fbda041c8f87152010d1f6aa364511c7f7d8b5bb
13 changes: 13 additions & 0 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member

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_EnableDeferredRefcount is thread-safe.

Copy link
Contributor Author

@LindaSummer LindaSummer Nov 17, 2025

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_EnableDeferredRefcount and find that it's thread-safe.

cpython/Objects/object.c

Lines 2734 to 2739 in 3d14805

if (_Py_atomic_compare_exchange_uint8(&op->ob_gc_bits, &bits, bits | _PyGC_BITS_DEFERRED) == 0)
{
// Someone beat us to it!
return 0;
}
_Py_atomic_add_ssize(&op->ob_ref_shared, _Py_REF_SHARED(_Py_REF_DEFERRED, 0));

It uses compare-and-set to guard the _PyGC_BITS_DEFERRED bit flag before updating the payload of the deferred refcount. So only one thread could make this change.

I removed the TYPE_LOCK and the double check of the _PyObject_HasDeferredRefcount. They are unnecessary.

Best Regards,
Edward

if (!_PyObject_HasDeferredRefcount(value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this checked twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ZeroIntensity ,

I tried to double-check the flag to reduce the conflict of entering the lock.
But as you mentioned above, we don't need a lock for PyUnstable_Object_EnableDeferredRefcount .
This double check has been removed with the lock.

An atomic action on the type flag is very efficient and is safe for our scenario.

Thanks very much for your suggestion!

Best Regards,
Edward

PyUnstable_Object_EnableDeferredRefcount(value);
}
END_TYPE_LOCK();
}
}
#endif

PyObject *old_value = NULL;
PyObject *descr = _PyType_LookupRef(metatype, name);
if (descr != NULL) {
Expand Down Expand Up @@ -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));
Expand Down
94 changes: 94 additions & 0 deletions edward-test-bench.py
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()