Skip to content

gh-130313: Avoid locking when clearing objects #130126

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

Merged
merged 3 commits into from
Feb 20, 2025
Merged
Changes from all commits
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
117 changes: 73 additions & 44 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -7163,6 +7163,17 @@ PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg)
return 0;
}

static void
clear_inline_values(PyDictValues *values)
{
if (values->valid) {
FT_ATOMIC_STORE_UINT8(values->valid, 0);
for (Py_ssize_t i = 0; i < values->capacity; i++) {
Py_CLEAR(values->values[i]);
}
}
}

static void
set_dict_inline_values(PyObject *obj, PyDictObject *new_dict)
{
Expand All @@ -7173,12 +7184,7 @@ set_dict_inline_values(PyObject *obj, PyDictObject *new_dict)
Py_XINCREF(new_dict);
FT_ATOMIC_STORE_PTR(_PyObject_ManagedDictPointer(obj)->dict, new_dict);

if (values->valid) {
FT_ATOMIC_STORE_UINT8(values->valid, 0);
for (Py_ssize_t i = 0; i < values->capacity; i++) {
Py_CLEAR(values->values[i]);
}
}
clear_inline_values(values);
}

#ifdef Py_GIL_DISABLED
Expand Down Expand Up @@ -7256,8 +7262,8 @@ decref_maybe_delay(PyObject *obj, bool delay)
}
}

static int
set_or_clear_managed_dict(PyObject *obj, PyObject *new_dict, bool clear)
int
_PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict)
{
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT);
#ifndef NDEBUG
Expand Down Expand Up @@ -7292,8 +7298,7 @@ set_or_clear_managed_dict(PyObject *obj, PyObject *new_dict, bool clear)

// Decref for the dictionary we incref'd in try_set_dict_inline_only_or_other_dict
// while the object was locked
decref_maybe_delay((PyObject *)prev_dict,
!clear && prev_dict != cur_dict);
decref_maybe_delay((PyObject *)prev_dict, prev_dict != cur_dict);
if (err != 0) {
return err;
}
Expand All @@ -7303,7 +7308,7 @@ set_or_clear_managed_dict(PyObject *obj, PyObject *new_dict, bool clear)

if (prev_dict != NULL) {
// decref for the dictionary that we replaced
decref_maybe_delay((PyObject *)prev_dict, !clear);
decref_maybe_delay((PyObject *)prev_dict, true);
}

return 0;
Expand Down Expand Up @@ -7333,45 +7338,15 @@ set_or_clear_managed_dict(PyObject *obj, PyObject *new_dict, bool clear)
(PyDictObject *)Py_XNewRef(new_dict));

Py_END_CRITICAL_SECTION();
decref_maybe_delay((PyObject *)dict, !clear);
decref_maybe_delay((PyObject *)dict, true);
}
assert(_PyObject_InlineValuesConsistencyCheck(obj));
return err;
}

int
_PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict)
{
return set_or_clear_managed_dict(obj, new_dict, false);
}

void
PyObject_ClearManagedDict(PyObject *obj)
{
if (set_or_clear_managed_dict(obj, NULL, true) < 0) {
/* Must be out of memory */
assert(PyErr_Occurred() == PyExc_MemoryError);
PyErr_FormatUnraisable("Exception ignored while "
"clearing an object managed dict");
/* Clear the dict */
PyDictObject *dict = _PyObject_GetManagedDict(obj);
Py_BEGIN_CRITICAL_SECTION2(dict, obj);
dict = _PyObject_ManagedDictPointer(obj)->dict;
PyInterpreterState *interp = _PyInterpreterState_GET();
PyDictKeysObject *oldkeys = dict->ma_keys;
set_keys(dict, Py_EMPTY_KEYS);
dict->ma_values = NULL;
dictkeys_decref(interp, oldkeys, IS_DICT_SHARED(dict));
STORE_USED(dict, 0);
set_dict_inline_values(obj, NULL);
Py_END_CRITICAL_SECTION2();
}
}

int
_PyDict_DetachFromObject(PyDictObject *mp, PyObject *obj)
static int
detach_dict_from_object(PyDictObject *mp, PyObject *obj)
{
ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj);
assert(_PyObject_ManagedDictPointer(obj)->dict == mp);
assert(_PyObject_InlineValuesConsistencyCheck(obj));

Expand Down Expand Up @@ -7401,6 +7376,60 @@ _PyDict_DetachFromObject(PyDictObject *mp, PyObject *obj)
return 0;
}


void
PyObject_ClearManagedDict(PyObject *obj)
{
// This is called when the object is being freed or cleared
// by the GC and therefore known to have no references.
if (Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
PyDictObject *dict = _PyObject_GetManagedDict(obj);
if (dict == NULL) {
// We have no materialized dictionary and inline values
// that just need to be cleared.
// No dict to clear, we're done
clear_inline_values(_PyObject_InlineValues(obj));
return;
}
else if (FT_ATOMIC_LOAD_PTR_RELAXED(dict->ma_values) ==
_PyObject_InlineValues(obj)) {
// We have a materialized object which points at the inline
// values. We need to materialize the keys. Nothing can modify
// this object, but we need to lock the dictionary.
int err;
Py_BEGIN_CRITICAL_SECTION(dict);
err = detach_dict_from_object(dict, obj);
Py_END_CRITICAL_SECTION();

if (err) {
/* Must be out of memory */
assert(PyErr_Occurred() == PyExc_MemoryError);
PyErr_FormatUnraisable("Exception ignored while "
"clearing an object managed dict");
/* Clear the dict */
Py_BEGIN_CRITICAL_SECTION(dict);
PyInterpreterState *interp = _PyInterpreterState_GET();
PyDictKeysObject *oldkeys = dict->ma_keys;
set_keys(dict, Py_EMPTY_KEYS);
dict->ma_values = NULL;
dictkeys_decref(interp, oldkeys, IS_DICT_SHARED(dict));
STORE_USED(dict, 0);
clear_inline_values(_PyObject_InlineValues(obj));
Py_END_CRITICAL_SECTION();
}
}
}
Py_CLEAR(_PyObject_ManagedDictPointer(obj)->dict);
}

int
_PyDict_DetachFromObject(PyDictObject *mp, PyObject *obj)
{
ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj);

return detach_dict_from_object(mp, obj);
}

static inline PyObject *
ensure_managed_dict(PyObject *obj)
{
Expand Down
Loading