From d3f79e57e8f6d4b5b980e05b8a394a40dc99b2d4 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 17 Apr 2025 08:50:14 -0700 Subject: [PATCH] Add optimization for non-interned type lookup. --- ...-04-17-11-32-43.gh-issue-132380.eDaVzm.rst | 3 + Objects/typeobject.c | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-04-17-11-32-43.gh-issue-132380.eDaVzm.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-17-11-32-43.gh-issue-132380.eDaVzm.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-17-11-32-43.gh-issue-132380.eDaVzm.rst new file mode 100644 index 00000000000000..71d29239ff10fd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-17-11-32-43.gh-issue-132380.eDaVzm.rst @@ -0,0 +1,3 @@ +Add an optimized implementation of type lookup for names that are +non-interned strings. This is only enabled for free-threaded builds since +that kind of lookup can cause contention for the global type lock. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index f65695360a7483..1dd3bd0cad5371 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -5728,6 +5728,50 @@ _PyTypes_AfterFork(void) #endif } +#ifdef Py_GIL_DISABLED +// Non-caching version of type lookup, containing a non-locking +// version of find_name_in_mro(). +static unsigned int +type_lookup_non_interned(PyTypeObject *type, PyObject *name, _PyStackRef *out) +{ + PyObject *mro = NULL; + Py_hash_t hash = _PyObject_HashFast(name); + if (hash == -1) { + goto error; + } + + /* Keep a strong reference to mro because type->tp_mro can be replaced + during dict lookup, e.g. when comparing to non-string keys. */ + mro = _PyType_GetMRO(type); + if (mro == NULL) { + assert(PyType_Ready(type)); + goto error; + } + + /* Look in tp_dict of types in MRO */ + PyObject *res = NULL; + Py_ssize_t n = PyTuple_GET_SIZE(mro); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *base = PyTuple_GET_ITEM(mro, i); + PyObject *dict = lookup_tp_dict(_PyType_CAST(base)); + assert(dict && PyDict_Check(dict)); + if (_PyDict_GetItemRef_KnownHash((PyDictObject *)dict, name, hash, &res) < 0) { + goto error; + } + if (res != NULL) { + break; + } + } + Py_DECREF(mro); + *out = res ? PyStackRef_FromPyObjectSteal(res) : PyStackRef_NULL; + return 0; +error: + Py_XDECREF(mro); + *out = PyStackRef_NULL; + return 0; +} +#endif + /* Internal API to look for a name through the MRO. This returns a strong reference, and doesn't set an exception! If nonzero, version is set to the value of type->tp_version at the time of @@ -5750,6 +5794,18 @@ _PyType_LookupRefAndVersion(PyTypeObject *type, PyObject *name, unsigned int *ve unsigned int _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out) { +#ifdef Py_GIL_DISABLED + // Bypass the type cache in this case since it is very unlikely it will do + // anything useful with a non-interned name lookup. This typically happens + // due to a getattr() call on a type with a name that has been constructed. + // We only have this path for the free-threaded build since cache misses are + // relatively more expensive for it and also to avoid contention on + // TYPE_LOCK. For the default build this extra branch is assumed to not be + // worth it, since this kind of lookup is quite rare. + if (!PyUnicode_CHECK_INTERNED(name) && _PyType_IsReady(type)) { + return type_lookup_non_interned(type, name, out); + } +#endif unsigned int h = MCACHE_HASH_METHOD(type, name); struct type_cache *cache = get_type_cache(); struct type_cache_entry *entry = &cache->hashtable[h];