gh-145685: Improve scaling of type attribute lookups (gh-145774)

Avoid locking in the PyType_Lookup cache-miss path if the type's
tp_version_tag is already valid.
This commit is contained in:
Sam Gross 2026-03-12 13:30:36 -04:00 committed by GitHub
parent 7a65900764
commit cd52172831
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 36 additions and 25 deletions

View file

@ -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) \
@ -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

View file

@ -0,0 +1,2 @@
Improve scaling of type attribute lookups in the :term:`free-threaded build` by
avoiding contention on the internal type lock.

View file

@ -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
@ -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)
{
@ -6182,24 +6190,20 @@ _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;
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);
}
res = find_name_in_mro(type, name, out);
END_TYPE_LOCK();
/* Only put NULL results into cache if there was no error. */
if (res < 0) {
@ -6207,16 +6211,18 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef
return 0;
}
if (has_version) {
PyObject *res_obj = PyStackRef_AsPyObjectBorrow(*out);
#if Py_GIL_DISABLED
update_cache_gil_disabled(entry, name, assigned_version, res_obj);
#else
PyObject *old_value = update_cache(entry, name, assigned_version, res_obj);
Py_DECREF(old_value);
#endif
if (version_tag == 0 || !MCACHE_CACHEABLE_NAME(name)) {
return 0;
}
return has_version ? assigned_version : 0;
PyObject *res_obj = PyStackRef_AsPyObjectBorrow(*out);
#if Py_GIL_DISABLED
update_cache_gil_disabled(entry, name, version_tag, res_obj);
#else
PyObject *old_value = update_cache(entry, name, version_tag, res_obj);
Py_DECREF(old_value);
#endif
return version_tag;
}
/* Internal API to look for a name through the MRO.