Skip to content

Commit 1509eb7

Browse files
colesburymiss-islington
authored andcommitted
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. (cherry picked from commit cd52172) Co-authored-by: Sam Gross <colesbury@gmail.com>
1 parent 6669b20 commit 1509eb7

File tree

3 files changed

+35
-24
lines changed

3 files changed

+35
-24
lines changed

Include/internal/pycore_pyatomic_ft_wrappers.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ extern "C" {
8787
_Py_atomic_store_int_relaxed(&value, new_value)
8888
#define FT_ATOMIC_LOAD_INT_RELAXED(value) \
8989
_Py_atomic_load_int_relaxed(&value)
90+
#define FT_ATOMIC_LOAD_UINT(value) \
91+
_Py_atomic_load_uint(&value)
9092
#define FT_ATOMIC_STORE_UINT_RELAXED(value, new_value) \
9193
_Py_atomic_store_uint_relaxed(&value, new_value)
9294
#define FT_ATOMIC_LOAD_UINT_RELAXED(value) \
@@ -158,6 +160,7 @@ extern "C" {
158160
#define FT_ATOMIC_STORE_INT(value, new_value) value = new_value
159161
#define FT_ATOMIC_LOAD_INT_RELAXED(value) value
160162
#define FT_ATOMIC_STORE_INT_RELAXED(value, new_value) value = new_value
163+
#define FT_ATOMIC_LOAD_UINT(value) value
161164
#define FT_ATOMIC_LOAD_UINT_RELAXED(value) value
162165
#define FT_ATOMIC_STORE_UINT_RELAXED(value, new_value) value = new_value
163166
#define FT_ATOMIC_LOAD_LONG_RELAXED(value) value
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve scaling of type attribute lookups in the :term:`free-threaded build` by
2+
avoiding contention on the internal type lock.

Objects/typeobject.c

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ class object "PyObject *" "&PyBaseObject_Type"
5151
MCACHE_HASH(FT_ATOMIC_LOAD_UINT32_RELAXED((type)->tp_version_tag), \
5252
((Py_ssize_t)(name)) >> 3)
5353
#define MCACHE_CACHEABLE_NAME(name) \
54-
PyUnicode_CheckExact(name) && \
55-
(PyUnicode_GET_LENGTH(name) <= MCACHE_MAX_ATTR_SIZE)
54+
(PyUnicode_CheckExact(name) && \
55+
(PyUnicode_GET_LENGTH(name) <= MCACHE_MAX_ATTR_SIZE))
5656

5757
#define NEXT_VERSION_TAG(interp) \
5858
(interp)->types.next_version_tag
@@ -5860,6 +5860,14 @@ _PyType_LookupRefAndVersion(PyTypeObject *type, PyObject *name, unsigned int *ve
58605860
return PyStackRef_AsPyObjectSteal(out);
58615861
}
58625862

5863+
static int
5864+
should_assign_version_tag(PyTypeObject *type, PyObject *name, unsigned int version_tag)
5865+
{
5866+
return (version_tag == 0
5867+
&& FT_ATOMIC_LOAD_UINT16_RELAXED(type->tp_versions_used) < MAX_VERSIONS_PER_CLASS
5868+
&& MCACHE_CACHEABLE_NAME(name));
5869+
}
5870+
58635871
unsigned int
58645872
_PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out)
58655873
{
@@ -5908,41 +5916,39 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef
59085916
/* We may end up clearing live exceptions below, so make sure it's ours. */
59095917
assert(!PyErr_Occurred());
59105918

5911-
// We need to atomically do the lookup and capture the version before
5912-
// anyone else can modify our mro or mutate the type.
5913-
59145919
int res;
59155920
PyInterpreterState *interp = _PyInterpreterState_GET();
5916-
int has_version = 0;
5917-
unsigned int assigned_version = 0;
59185921

5919-
BEGIN_TYPE_LOCK();
5920-
// We must assign the version before doing the lookup. If
5921-
// find_name_in_mro() blocks and releases the critical section
5922-
// then the type version can change.
5923-
if (MCACHE_CACHEABLE_NAME(name)) {
5924-
has_version = assign_version_tag(interp, type);
5925-
assigned_version = type->tp_version_tag;
5926-
}
5927-
res = find_name_in_mro(type, name, out);
5928-
END_TYPE_LOCK();
5922+
unsigned int version_tag = FT_ATOMIC_LOAD_UINT(type->tp_version_tag);
5923+
if (should_assign_version_tag(type, name, version_tag)) {
5924+
BEGIN_TYPE_LOCK();
5925+
assign_version_tag(interp, type);
5926+
version_tag = type->tp_version_tag;
5927+
res = find_name_in_mro(type, name, out);
5928+
END_TYPE_LOCK();
5929+
}
5930+
else {
5931+
res = find_name_in_mro(type, name, out);
5932+
}
59295933

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

5936-
if (has_version) {
5937-
PyObject *res_obj = PyStackRef_AsPyObjectBorrow(*out);
5940+
if (version_tag == 0 || !MCACHE_CACHEABLE_NAME(name)) {
5941+
return 0;
5942+
}
5943+
5944+
PyObject *res_obj = PyStackRef_AsPyObjectBorrow(*out);
59385945
#if Py_GIL_DISABLED
5939-
update_cache_gil_disabled(entry, name, assigned_version, res_obj);
5946+
update_cache_gil_disabled(entry, name, version_tag, res_obj);
59405947
#else
5941-
PyObject *old_value = update_cache(entry, name, assigned_version, res_obj);
5942-
Py_DECREF(old_value);
5948+
PyObject *old_value = update_cache(entry, name, version_tag, res_obj);
5949+
Py_DECREF(old_value);
59435950
#endif
5944-
}
5945-
return has_version ? assigned_version : 0;
5951+
return version_tag;
59465952
}
59475953

59485954
/* Internal API to look for a name through the MRO.

0 commit comments

Comments
 (0)