Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions Include/internal/pycore_pyatomic_ft_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ extern "C" {
_Py_atomic_store_int_relaxed(&value, new_value)
#define FT_ATOMIC_LOAD_INT_RELAXED(value) \
_Py_atomic_load_int_relaxed(&value)
#define FT_ATOMIC_LOAD_UINT(value) \
_Py_atomic_load_uint(&value)
#define FT_ATOMIC_STORE_UINT_RELAXED(value, new_value) \
_Py_atomic_store_uint_relaxed(&value, new_value)
#define FT_ATOMIC_LOAD_UINT_RELAXED(value) \
Expand Down Expand Up @@ -167,6 +169,7 @@ extern "C" {
#define FT_ATOMIC_STORE_INT(value, new_value) value = new_value
#define FT_ATOMIC_LOAD_INT_RELAXED(value) value
#define FT_ATOMIC_STORE_INT_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_LOAD_UINT(value) value
#define FT_ATOMIC_LOAD_UINT_RELAXED(value) value
#define FT_ATOMIC_STORE_UINT_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_LOAD_LONG_RELAXED(value) value
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve scaling of type attribute lookups in the :term:`free-threaded build` by
avoiding contention on the internal type lock.
54 changes: 30 additions & 24 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ class object "PyObject *" "&PyBaseObject_Type"
MCACHE_HASH(FT_ATOMIC_LOAD_UINT_RELAXED((type)->tp_version_tag), \
((Py_ssize_t)(name)) >> 3)
#define MCACHE_CACHEABLE_NAME(name) \
PyUnicode_CheckExact(name) && \
(PyUnicode_GET_LENGTH(name) <= MCACHE_MAX_ATTR_SIZE)
(PyUnicode_CheckExact(name) && \
(PyUnicode_GET_LENGTH(name) <= MCACHE_MAX_ATTR_SIZE))

#define NEXT_VERSION_TAG(interp) \
(interp)->types.next_version_tag
Expand Down Expand Up @@ -6134,6 +6134,14 @@ _PyType_LookupRefAndVersion(PyTypeObject *type, PyObject *name, unsigned int *ve
return PyStackRef_AsPyObjectSteal(out);
}

static int
should_assign_version_tag(PyTypeObject *type, PyObject *name, unsigned int version_tag)
{
return (version_tag == 0
&& FT_ATOMIC_LOAD_UINT16_RELAXED(type->tp_versions_used) < MAX_VERSIONS_PER_CLASS
&& MCACHE_CACHEABLE_NAME(name));
}

unsigned int
_PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out)
{
Expand Down Expand Up @@ -6182,41 +6190,39 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef
/* We may end up clearing live exceptions below, so make sure it's ours. */
assert(!PyErr_Occurred());

// We need to atomically do the lookup and capture the version before
// anyone else can modify our mro or mutate the type.

int res;
PyInterpreterState *interp = _PyInterpreterState_GET();
int has_version = 0;
unsigned int assigned_version = 0;

BEGIN_TYPE_LOCK();
// We must assign the version before doing the lookup. If
// find_name_in_mro() blocks and releases the critical section
// then the type version can change.
if (MCACHE_CACHEABLE_NAME(name)) {
has_version = assign_version_tag(interp, type);
assigned_version = type->tp_version_tag;
}
res = find_name_in_mro(type, name, out);
END_TYPE_LOCK();
unsigned int version_tag = FT_ATOMIC_LOAD_UINT(type->tp_version_tag);
if (should_assign_version_tag(type, name, version_tag)) {
BEGIN_TYPE_LOCK();
assign_version_tag(interp, type);
version_tag = type->tp_version_tag;
res = find_name_in_mro(type, name, out);
END_TYPE_LOCK();
}
else {
res = find_name_in_mro(type, name, out);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to hold the type lock around this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The operations in find_name_in_mro are individually thread-safe, although this allows for mutations to types to be interleaved with the MRO traversal. I think that's acceptable. There were already edge cases where that could happen due to ensure_shared_on_read() calls suspending the critical section during dictionary lookups in find_name_in_mro.

Importantly, if any type in the MRO chain is modified during lookup, the assigned version tag will be stale so any future cache lookups won't use it.

}

/* Only put NULL results into cache if there was no error. */
if (res < 0) {
*out = PyStackRef_NULL;
return 0;
}

if (has_version) {
PyObject *res_obj = PyStackRef_AsPyObjectBorrow(*out);
if (version_tag == 0 || !MCACHE_CACHEABLE_NAME(name)) {
return 0;
}

PyObject *res_obj = PyStackRef_AsPyObjectBorrow(*out);
#if Py_GIL_DISABLED
update_cache_gil_disabled(entry, name, assigned_version, res_obj);
update_cache_gil_disabled(entry, name, version_tag, res_obj);
#else
PyObject *old_value = update_cache(entry, name, assigned_version, res_obj);
Py_DECREF(old_value);
PyObject *old_value = update_cache(entry, name, version_tag, res_obj);
Py_DECREF(old_value);
#endif
}
return has_version ? assigned_version : 0;
return version_tag;
}

/* Internal API to look for a name through the MRO.
Expand Down
Loading