From 36ffc5cb185249cce8665f8c6d6836352ee03a45 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 15 May 2026 18:26:18 +0530 Subject: [PATCH 01/11] implement per type method cache --- Include/cpython/object.h | 2 + Include/internal/pycore_interp_structs.h | 23 +-- Include/internal/pycore_object.h | 2 - Include/internal/pycore_typecache.h | 44 +++++ Lib/test/test_free_threading/test_type.py | 18 ++ Lib/test/test_sys.py | 2 +- Makefile.pre.in | 2 + Objects/typeobject.c | 209 +++------------------- PCbuild/_freeze_module.vcxproj | 1 + PCbuild/_freeze_module.vcxproj.filters | 3 + PCbuild/pythoncore.vcxproj | 2 + PCbuild/pythoncore.vcxproj.filters | 6 + Python/pystate.c | 3 +- Python/typecache.c | 186 +++++++++++++++++++ Tools/ftscalingbench/ftscalingbench.py | 16 ++ 15 files changed, 314 insertions(+), 205 deletions(-) create mode 100644 Include/internal/pycore_typecache.h create mode 100644 Python/typecache.c diff --git a/Include/cpython/object.h b/Include/cpython/object.h index 326254c335b4895..4c5a677e5543ece 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -246,6 +246,8 @@ struct _typeobject { * This function must escape to any code that can result in * the GC being run, such as Py_DECREF. */ _Py_iteritemfunc _tp_iteritem; + + void *_tp_cache; }; #define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index f13bc2178b1e7eb..1c0ea07d2843bca 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -548,23 +548,6 @@ struct _types_runtime_state { }; -// Type attribute lookup cache: speed up attribute and method lookups, -// see _PyType_Lookup(). -struct type_cache_entry { - unsigned int version; // initialized from type->tp_version_tag -#ifdef Py_GIL_DISABLED - _PySeqLock sequence; -#endif - PyObject *name; // reference to exactly a str or None - PyObject *value; // borrowed reference or NULL -}; - -#define MCACHE_SIZE_EXP 12 - -struct type_cache { - struct type_cache_entry hashtable[1 << MCACHE_SIZE_EXP]; -}; - typedef struct { PyTypeObject *type; int isbuiltin; @@ -579,6 +562,10 @@ typedef struct { are also some diagnostic uses for the list of weakrefs, so we still keep it. */ PyObject *tp_weaklist; + /* Per-interpreter attribute lookup cache (struct type_cache *). + For static builtin types the cache must be per-interpreter + because tp_dict and the values it stores are per-interpreter. */ + void *_tp_cache; } managed_static_type_state; #define TYPE_VERSION_CACHE_SIZE (1<<12) /* Must be a power of 2 */ @@ -589,8 +576,6 @@ struct types_state { where all those lower numbers are used for core static types. */ unsigned int next_version_tag; - struct type_cache type_cache; - /* Every static builtin type is initialized for each interpreter during its own initialization, including for the main interpreter during global runtime initialization. This is done by calling diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index c2c508c1a71c5c0..8fa3b47b6c312ac 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -291,8 +291,6 @@ _PyType_HasFeature(PyTypeObject *type, unsigned long feature) { return ((type->tp_flags) & feature) != 0; } -extern void _PyType_InitCache(PyInterpreterState *interp); - extern PyStatus _PyObject_InitState(PyInterpreterState *interp); extern void _PyObject_FiniState(PyInterpreterState *interp); extern bool _PyRefchain_IsTraced(PyInterpreterState *interp, PyObject *obj); diff --git a/Include/internal/pycore_typecache.h b/Include/internal/pycore_typecache.h new file mode 100644 index 000000000000000..2af68c204476568 --- /dev/null +++ b/Include/internal/pycore_typecache.h @@ -0,0 +1,44 @@ +#ifndef PY_INTERNAL_TYPECACHE_H +#define PY_INTERNAL_TYPECACHE_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include "pycore_stackref.h" + + +#define _Py_TYPECACHE_MINSIZE 8 + +struct type_cache_entry { + PyObject *name; + PyObject *value; +}; + +struct type_cache { + uint32_t mask; + uint32_t version_tag; + uint32_t available; + uint32_t used; + struct type_cache_entry hashtable[0]; +}; + +struct _PyTypeCacheLookupResult { + _PyStackRef value; + int cache_hit; + uint32_t version_tag; +}; + + +extern void _PyTypeCache_InitType(PyTypeObject *type); +extern void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value); +extern struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name); +extern void _PyTypeCache_Invalidate(PyTypeObject *type); + +#ifdef __cplusplus +} +#endif +#endif /* PY_INTERNAL_TYPECACHE_H */ diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py index 1255d842dbff48f..f7bacab00846b99 100644 --- a/Lib/test/test_free_threading/test_type.py +++ b/Lib/test/test_free_threading/test_type.py @@ -84,6 +84,24 @@ def reader_func(): self.run_one(writer_func, reader_func) + def test_attr_cache_mortal(self): + class C: + x = object() + + class D(C): + pass + + def writer_func(): + for _ in range(3000): + C.x = object() + + def reader_func(): + for _ in range(3000): + C.x + D.x + + self.run_one(writer_func, reader_func) + def test___class___modification(self): loops = 200 diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 02c70403185f60d..75347f59f4adf4d 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1788,7 +1788,7 @@ def delx(self): del self.__x check((1,2,3), vsize('') + self.P + 3*self.P) # type # static type: PyTypeObject - fmt = 'P2nPI13Pl4Pn9Pn12PI2Pc' + fmt = 'P2nPI13Pl4Pn9Pn12PI2PcP' s = vsize(fmt) check(int, s) typeid = 'n' if support.Py_GIL_DISABLED else '' diff --git a/Makefile.pre.in b/Makefile.pre.in index 9435bf534fb5121..eebcf45e442e882 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -506,6 +506,7 @@ PYTHON_OBJS= \ Python/thread.o \ Python/traceback.o \ Python/tracemalloc.o \ + Python/typecache.o \ Python/uniqueid.o \ Python/getopt.o \ Python/pystrcmp.o \ @@ -1411,6 +1412,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_tracemalloc.h \ $(srcdir)/Include/internal/pycore_tstate.h \ $(srcdir)/Include/internal/pycore_tuple.h \ + $(srcdir)/Include/internal/pycore_typecache.h \ $(srcdir)/Include/internal/pycore_typedefs.h \ $(srcdir)/Include/internal/pycore_typeobject.h \ $(srcdir)/Include/internal/pycore_typevarobject.h \ diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 7cca137f74be58f..1faa30ea2ae8ae7 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -21,7 +21,8 @@ #include "pycore_slots.h" // _PySlotIterator_Init #include "pycore_symtable.h" // _Py_Mangle() #include "pycore_tuple.h" // _PyTuple_FromPair -#include "pycore_typeobject.h" // struct type_cache +#include "pycore_typecache.h" // _PyTypeCache_Lookup() +#include "pycore_typeobject.h" // _PyTypes_InitTypes() #include "pycore_unicodeobject.h" // _PyUnicode_Copy #include "pycore_unionobject.h" // _Py_union_type_or #include "pycore_weakref.h" // _PyWeakref_GET_REF() @@ -41,21 +42,7 @@ class object "PyObject *" "&PyBaseObject_Type" /* Support type attribute lookup cache */ -/* The cache can keep references to the names alive for longer than - they normally would. This is why the maximum size is limited to - MCACHE_MAX_ATTR_SIZE, since it might be a problem if very large - strings are used as attribute names. */ -#define MCACHE_MAX_ATTR_SIZE 100 -#define MCACHE_HASH(version, name_hash) \ - (((unsigned int)(version) ^ (unsigned int)(name_hash)) \ - & ((1 << MCACHE_SIZE_EXP) - 1)) - -#define MCACHE_HASH_METHOD(type, name) \ - 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)) +#define MCACHE_CACHEABLE_NAME(name) (PyUnicode_CheckExact(name) && PyUnicode_CHECK_INTERNED(name)) #define NEXT_VERSION_TAG(interp) \ (interp)->types.next_version_tag @@ -969,75 +956,18 @@ _PyType_GetTextSignatureFromInternalDoc(const char *name, const char *internal_d } -static struct type_cache* -get_type_cache(void) -{ - PyInterpreterState *interp = _PyInterpreterState_GET(); - return &interp->types.type_cache; -} - - -static void -type_cache_clear(struct type_cache *cache, PyObject *value) -{ - for (Py_ssize_t i = 0; i < (1 << MCACHE_SIZE_EXP); i++) { - struct type_cache_entry *entry = &cache->hashtable[i]; -#ifdef Py_GIL_DISABLED - _PySeqLock_LockWrite(&entry->sequence); -#endif - entry->version = 0; - Py_XSETREF(entry->name, _Py_XNewRef(value)); - entry->value = NULL; -#ifdef Py_GIL_DISABLED - _PySeqLock_UnlockWrite(&entry->sequence); -#endif - } -} - - -void -_PyType_InitCache(PyInterpreterState *interp) -{ - struct type_cache *cache = &interp->types.type_cache; - for (Py_ssize_t i = 0; i < (1 << MCACHE_SIZE_EXP); i++) { - struct type_cache_entry *entry = &cache->hashtable[i]; - assert(entry->name == NULL); - - entry->version = 0; - // Set to None so _PyType_LookupRef() can use Py_SETREF(), - // rather than using slower Py_XSETREF(). - entry->name = Py_None; - entry->value = NULL; - } -} - - -static unsigned int -_PyType_ClearCache(PyInterpreterState *interp) -{ - struct type_cache *cache = &interp->types.type_cache; - // Set to None, rather than NULL, so _PyType_LookupRef() can - // use Py_SETREF() rather than using slower Py_XSETREF(). - type_cache_clear(cache, Py_None); - - return NEXT_VERSION_TAG(interp) - 1; -} - - unsigned int PyType_ClearCache(void) { PyInterpreterState *interp = _PyInterpreterState_GET(); - return _PyType_ClearCache(interp); + + return NEXT_VERSION_TAG(interp) - 1; } void _PyTypes_Fini(PyInterpreterState *interp) { - struct type_cache *cache = &interp->types.type_cache; - type_cache_clear(cache, NULL); - // All the managed static types should have been finalized already. assert(interp->types.for_extensions.num_initialized == 0); for (size_t i = 0; i < _Py_MAX_MANAGED_STATIC_EXT_TYPES; i++) { @@ -1231,6 +1161,7 @@ type_modified_unlocked(PyTypeObject *type) } set_version_unlocked(type, 0); /* 0 is not a valid version tag */ + _PyTypeCache_Invalidate(type); if (PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) { // This field *must* be invalidated if the type is modified (see the // comment on struct _specialization_cache): @@ -1314,6 +1245,7 @@ type_mro_modified(PyTypeObject *type, PyObject *bases) clear: assert(!(type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN)); set_version_unlocked(type, 0); /* 0 is not a valid version tag */ + _PyTypeCache_Invalidate(type); type->tp_versions_used = _Py_ATTR_CACHE_UNUSED; if (PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) { // This field *must* be invalidated if the type is modified (see the @@ -6197,67 +6129,9 @@ is_dunder_name(PyObject *name) return 0; } -static PyObject * -update_cache(struct type_cache_entry *entry, PyObject *name, unsigned int version_tag, PyObject *value) -{ - _Py_atomic_store_ptr_relaxed(&entry->value, value); /* borrowed */ - assert(PyUnstable_Unicode_GET_CACHED_HASH(name) != -1); - OBJECT_STAT_INC_COND(type_cache_collisions, entry->name != Py_None && entry->name != name); - // We're releasing this under the lock for simplicity sake because it's always a - // exact unicode object or Py_None so it's safe to do so. - PyObject *old_name = entry->name; - _Py_atomic_store_ptr_relaxed(&entry->name, Py_NewRef(name)); - // We must write the version last to avoid _Py_TryXGetStackRef() - // operating on an invalid (already deallocated) value inside - // _PyType_LookupRefAndVersion(). If we write the version first then a - // reader could pass the "entry_version == type_version" check but could - // be using the old entry value. - _Py_atomic_store_uint32_release(&entry->version, version_tag); - return old_name; -} - -#if Py_GIL_DISABLED - -static void -update_cache_gil_disabled(struct type_cache_entry *entry, PyObject *name, - unsigned int version_tag, PyObject *value) -{ - _PySeqLock_LockWrite(&entry->sequence); - - // update the entry - if (entry->name == name && - entry->value == value && - entry->version == version_tag) { - // We raced with another update, bail and restore previous sequence. - _PySeqLock_AbandonWrite(&entry->sequence); - return; - } - - PyObject *old_value = update_cache(entry, name, version_tag, value); - - // Then update sequence to the next valid value - _PySeqLock_UnlockWrite(&entry->sequence); - - Py_DECREF(old_value); -} - -#endif - void _PyTypes_AfterFork(void) { -#ifdef Py_GIL_DISABLED - struct type_cache *cache = get_type_cache(); - for (Py_ssize_t i = 0; i < (1 << MCACHE_SIZE_EXP); i++) { - struct type_cache_entry *entry = &cache->hashtable[i]; - if (_PySeqLock_AfterFork(&entry->sequence)) { - // Entry was in the process of updating while forking, clear it... - entry->value = NULL; - Py_SETREF(entry->name, Py_None); - entry->version = 0; - } - } -#endif } /* Internal API to look for a name through the MRO. @@ -6290,45 +6164,16 @@ should_assign_version_tag(PyTypeObject *type, PyObject *name, unsigned int versi unsigned int _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out) { - unsigned int h = MCACHE_HASH_METHOD(type, name); - struct type_cache *cache = get_type_cache(); - struct type_cache_entry *entry = &cache->hashtable[h]; -#ifdef Py_GIL_DISABLED - // synchronize-with other writing threads by doing an acquire load on the sequence - while (1) { - uint32_t sequence = _PySeqLock_BeginRead(&entry->sequence); - uint32_t entry_version = _Py_atomic_load_uint32_acquire(&entry->version); - uint32_t type_version = _Py_atomic_load_uint32_acquire(&type->tp_version_tag); - if (entry_version == type_version && - _Py_atomic_load_ptr_relaxed(&entry->name) == name) { + int cacheable = MCACHE_CACHEABLE_NAME(name); + if (cacheable) { + struct _PyTypeCacheLookupResult r = _PyTypeCache_Lookup(type, name); + if (r.cache_hit) { OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name)); OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name)); - if (_Py_TryXGetStackRef(&entry->value, out)) { - // If the sequence is still valid then we're done - if (_PySeqLock_EndRead(&entry->sequence, sequence)) { - return entry_version; - } - PyStackRef_XCLOSE(*out); - } - else { - // If we can't incref the object we need to fallback to locking - break; - } - } - else { - // cache miss - break; + *out = r.value; + return r.version_tag; } } -#else - if (entry->version == type->tp_version_tag && entry->name == name) { - assert(type->tp_version_tag); - OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name)); - OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name)); - *out = entry->value ? PyStackRef_FromPyObjectNew(entry->value) : PyStackRef_NULL; - return entry->version; - } -#endif OBJECT_STAT_INC_COND(type_cache_misses, !is_dunder_name(name)); OBJECT_STAT_INC_COND(type_cache_dunder_misses, is_dunder_name(name)); @@ -6339,14 +6184,23 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef PyInterpreterState *interp = _PyInterpreterState_GET(); unsigned int version_tag = FT_ATOMIC_LOAD_UINT(type->tp_version_tag); - if (should_assign_version_tag(type, name, version_tag)) { + if (cacheable && + (version_tag != 0 || should_assign_version_tag(type, name, version_tag))) + { BEGIN_TYPE_LOCK(); - assign_version_tag(interp, type); version_tag = type->tp_version_tag; + if (version_tag == 0) { + assign_version_tag(interp, type); + version_tag = type->tp_version_tag; + } res = find_name_in_mro(type, name, out); + if (res >= 0 && version_tag != 0) { + _PyTypeCache_Insert(type, name, PyStackRef_AsPyObjectBorrow(*out)); + } END_TYPE_LOCK(); } else { + version_tag = 0; res = find_name_in_mro(type, name, out); } @@ -6356,17 +6210,6 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef return 0; } - 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, version_tag, res_obj); -#else - PyObject *old_value = update_cache(entry, name, version_tag, res_obj); - Py_DECREF(old_value); -#endif return version_tag; } @@ -6881,6 +6724,7 @@ clear_static_type_objects(PyInterpreterState *interp, PyTypeObject *type, if (final) { Py_CLEAR(type->tp_cache); } + _PyTypeCache_Invalidate(type); clear_tp_dict(type); clear_tp_bases(type, final); clear_tp_mro(type, final); @@ -6990,6 +6834,7 @@ type_dealloc(PyObject *self) Py_XDECREF(type->tp_bases); Py_XDECREF(type->tp_mro); Py_XDECREF(type->tp_cache); + _PyTypeCache_Invalidate(type); clear_tp_subclasses(type); /* A type's tp_doc is heap allocated, unlike the tp_doc slots @@ -9505,6 +9350,8 @@ type_ready(PyTypeObject *type, int initial) goto error; } + _PyTypeCache_InitType(type); + #ifdef Py_TRACE_REFS /* PyType_Ready is the closest thing we have to a choke point * for type objects, so is the best place I can think of to try diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj index 17b98c9d9ec3455..e6c0ae16a79986c 100644 --- a/PCbuild/_freeze_module.vcxproj +++ b/PCbuild/_freeze_module.vcxproj @@ -282,6 +282,7 @@ + diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters index af3fded0dabf2d6..28bac4e8e8a5a34 100644 --- a/PCbuild/_freeze_module.vcxproj.filters +++ b/PCbuild/_freeze_module.vcxproj.filters @@ -490,6 +490,9 @@ Source Files + + Source Files + Source Files diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index e255ed5af19125d..9b8bdde8c8d9be0 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -332,6 +332,7 @@ + @@ -700,6 +701,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 649ee1859ff9961..7788871ea51a3a3 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -894,6 +894,9 @@ Include\internal + + Include\internal + Include\internal @@ -1613,6 +1616,9 @@ Python + + Python + Python diff --git a/Python/pystate.c b/Python/pystate.c index ff712019affbf9e..0c17a9901f195a4 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -12,7 +12,7 @@ #include "pycore_freelist.h" // _PyObject_ClearFreeLists() #include "pycore_initconfig.h" // _PyStatus_OK() #include "pycore_interpframe.h" // _PyThreadState_HasStackSpace() -#include "pycore_object.h" // _PyType_InitCache() +#include "pycore_object.h" // _PyObject_GC_New() #include "pycore_obmalloc.h" // _PyMem_obmalloc_state_on_heap() #include "pycore_optimizer.h" // JIT_CLEANUP_THRESHOLD #include "pycore_parking_lot.h" // _PyParkingLot_AfterFork() @@ -572,7 +572,6 @@ init_interpreter(PyInterpreterState *interp, _PyEval_InitState(interp); _PyGC_InitState(&interp->gc); PyConfig_InitPythonConfig(&interp->config); - _PyType_InitCache(interp); #ifdef Py_GIL_DISABLED _Py_brc_init_state(interp); #endif diff --git a/Python/typecache.c b/Python/typecache.c new file mode 100644 index 000000000000000..4d74d9d6b05f546 --- /dev/null +++ b/Python/typecache.c @@ -0,0 +1,186 @@ +#include "Python.h" + +#include "pycore_typecache.h" +#include "pycore_interp.h" // PyInterpreterState +#include "pycore_object.h" // _PyObject_XDecRefDelayed() +#include "pycore_pymem.h" +#include "pycore_pystate.h" // _PyInterpreterState_GET() +#include "pycore_pyatomic_ft_wrappers.h" +#include "pycore_typeobject.h" // _PyStaticType_GetState() + +static struct { + struct type_cache cache; + struct type_cache_entry entries[_Py_TYPECACHE_MINSIZE]; +} empty_cache_storage = { + .cache = { + .mask = _Py_TYPECACHE_MINSIZE - 1, + .available = 0, + .used = 0, + }, +}; + +#define empty_cache (empty_cache_storage.cache) + +static inline uint32_t cache_size(struct type_cache *cache) +{ + return cache->mask + 1; +} + +static inline size_t cache_nbytes(struct type_cache *cache) +{ + return sizeof(struct type_cache) + + (size_t)cache_size(cache) * sizeof(struct type_cache_entry); +} + +static struct type_cache *allocate_cache(uint32_t size) +{ + assert((size & (size - 1)) == 0); + struct type_cache *cache = PyMem_Calloc(1, sizeof(struct type_cache) + size * sizeof(struct type_cache_entry)); + if (cache == NULL) { + return NULL; + } + cache->mask = size - 1; + cache->available = size - (size >> 2); + cache->used = 0; + return cache; +} + +static void free_cache_delayed(struct type_cache *cache) +{ + if (cache == NULL || cache == &empty_cache) { + return; + } + _PyMem_FreeDelayed(cache, cache_nbytes(cache)); +} + + +static inline void **cache_slot(PyTypeObject *type) +{ + if (type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) { + PyInterpreterState *interp = _PyInterpreterState_GET(); + managed_static_type_state *state = _PyStaticType_GetState(interp, type); + assert(state != NULL); + return &state->_tp_cache; + } + return &type->_tp_cache; +} + +static inline struct type_cache *get_cache(PyTypeObject *type) +{ + return (struct type_cache *)FT_ATOMIC_LOAD_PTR(*cache_slot(type)); +} + +static inline void set_cache(PyTypeObject *type, struct type_cache *cache) +{ + FT_ATOMIC_STORE_PTR(*cache_slot(type), cache); +} + +void _PyTypeCache_InitType(PyTypeObject *type) +{ + *cache_slot(type) = &empty_cache; +} + +static inline void type_cache_insert(struct type_cache *cache, PyObject *name, + PyObject *value) +{ + Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(name); + assert(hash != -1); + uint32_t index = hash & cache->mask; + for (;;) { + if (cache->hashtable[index].name == NULL) { + FT_ATOMIC_STORE_PTR(cache->hashtable[index].value, value); + FT_ATOMIC_STORE_PTR(cache->hashtable[index].name, name); + cache->used++; + cache->available--; + return; + } + else if (cache->hashtable[index].name == name) { + return; + } + index = (index + 1) & cache->mask; + } +} + +static inline int type_cache_resize(PyTypeObject *type, struct type_cache *cache) +{ + uint32_t old_size = cache_size(cache); + uint32_t new_size; + if (cache->used == 0) { + new_size = _Py_TYPECACHE_MINSIZE; + } + else { + new_size = old_size * 2; + } + struct type_cache *new_cache = allocate_cache(new_size); + if (new_cache == NULL) { + return -1; + } + FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); + for (uint32_t i = 0; i < old_size; i++) { + if (cache->hashtable[i].name != NULL) { + type_cache_insert(new_cache, cache->hashtable[i].name, + cache->hashtable[i].value); + } + } + set_cache(type, new_cache); + free_cache_delayed(cache); + return 0; +} + +void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) +{ + struct type_cache *cache = get_cache(type); + if (cache->available == 0) { + if (type_cache_resize(type, cache) == -1) { + return; + } + cache = get_cache(type); + assert(cache->available > 0); + } + type_cache_insert(cache, name, value); + FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); +} + +struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name) +{ + assert(PyUnicode_CheckExact(name) && PyUnicode_CHECK_INTERNED(name)); + struct _PyTypeCacheLookupResult miss = {PyStackRef_NULL, 0, 0}; + struct type_cache *cache = get_cache(type); + if (cache == NULL) { + return miss; + } + Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(name); + assert(hash != -1); + uint32_t index = hash & cache->mask; + _PyStackRef out_ref; + for (;;) { + PyObject *entry_name = FT_ATOMIC_LOAD_PTR(cache->hashtable[index].name); + if (entry_name == NULL) { + return miss; + } + if (entry_name == name) { +#ifdef Py_GIL_DISABLED + if (!_Py_TryXGetStackRef(&cache->hashtable[index].value, &out_ref)) { + return miss; + } +#else + PyObject *v = cache->hashtable[index].value; + out_ref = v ? PyStackRef_FromPyObjectNew(v) : PyStackRef_NULL; +#endif + break; + } + index = (index + 1) & cache->mask; + } + uint32_t cache_version = FT_ATOMIC_LOAD_UINT_RELAXED(cache->version_tag); + if (cache_version != FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)) { + PyStackRef_XCLOSE(out_ref); + return miss; + } + return (struct _PyTypeCacheLookupResult){out_ref, 1, cache_version}; +} + +void _PyTypeCache_Invalidate(PyTypeObject *type) { + struct type_cache *cache = get_cache(type); + set_cache(type, &empty_cache); + free_cache_delayed(cache); +} diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index c8a914c22a9e137..824dd822854449a 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -326,6 +326,22 @@ def enum_attr(): MyEnum.Z +_MCACHE_NUM_TYPES = 1 << 14 +_MCACHE_PAIRS = [ + (type(f"C{i}", (), {f"m{i}": lambda self: None})(), f"m{i}") + for i in range(_MCACHE_NUM_TYPES) +] + +@register_benchmark +def type_lookup(): + pairs = _MCACHE_PAIRS + n = len(pairs) + outer = (1000 * WORK_SCALE) // n + for _ in range(outer): + for inst, name in pairs: + getattr(inst, name) + + def bench_one_thread(func): t0 = time.perf_counter_ns() func() From a4b07af14940ade0f5fe35ff1a0619e90aa78d07 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 20 May 2026 20:06:35 +0530 Subject: [PATCH 02/11] add micro benchmark --- Tools/ftscalingbench/ftscalingbench.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index 824dd822854449a..a79242e740371ba 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -325,19 +325,16 @@ def enum_attr(): MyEnum.Y MyEnum.Z - _MCACHE_NUM_TYPES = 1 << 14 _MCACHE_PAIRS = [ - (type(f"C{i}", (), {f"m{i}": lambda self: None})(), f"m{i}") + (type(f"C{i}", (), {f"m{i}": i % 256})(), sys.intern(f"m{i}")) for i in range(_MCACHE_NUM_TYPES) ] @register_benchmark def type_lookup(): pairs = _MCACHE_PAIRS - n = len(pairs) - outer = (1000 * WORK_SCALE) // n - for _ in range(outer): + for _ in range(WORK_SCALE // 10): for inst, name in pairs: getattr(inst, name) From 6b55b9d73f3b3cbbdee9827529e685262e83c765 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 20 May 2026 20:35:58 +0530 Subject: [PATCH 03/11] fix issie with zero length arrays --- Include/internal/pycore_typecache.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/internal/pycore_typecache.h b/Include/internal/pycore_typecache.h index 2af68c204476568..da805ca33a1cb85 100644 --- a/Include/internal/pycore_typecache.h +++ b/Include/internal/pycore_typecache.h @@ -23,7 +23,7 @@ struct type_cache { uint32_t version_tag; uint32_t available; uint32_t used; - struct type_cache_entry hashtable[0]; + struct type_cache_entry hashtable[1]; }; struct _PyTypeCacheLookupResult { From c9bdf4b2f5dff22a965683eaf49e5cb12bdcafd9 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 20 May 2026 21:08:31 +0530 Subject: [PATCH 04/11] fix for default build --- Python/typecache.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Python/typecache.c b/Python/typecache.c index 4d74d9d6b05f546..870d6ce86297954 100644 --- a/Python/typecache.c +++ b/Python/typecache.c @@ -50,6 +50,13 @@ static void free_cache_delayed(struct type_cache *cache) if (cache == NULL || cache == &empty_cache) { return; } +#ifndef Py_GIL_DISABLED + for (uint32_t i = 0; i < cache_size(cache); i++) { + if (cache->hashtable[i].name != NULL) { + Py_DECREF(cache->hashtable[i].name); + } + } +#endif _PyMem_FreeDelayed(cache, cache_nbytes(cache)); } @@ -88,6 +95,7 @@ static inline void type_cache_insert(struct type_cache *cache, PyObject *name, uint32_t index = hash & cache->mask; for (;;) { if (cache->hashtable[index].name == NULL) { + Py_INCREF(name); FT_ATOMIC_STORE_PTR(cache->hashtable[index].value, value); FT_ATOMIC_STORE_PTR(cache->hashtable[index].name, name); cache->used++; From bd59f2d2f6f819200c5957409c6793ec2d758414 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 21 May 2026 21:48:36 +0530 Subject: [PATCH 05/11] add comments --- Objects/typeobject.c | 5 ++++- Python/typecache.c | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 1faa30ea2ae8ae7..8305dd878642771 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6652,7 +6652,10 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) done: Py_DECREF(name); Py_XDECREF(descr); - Py_XDECREF(old_value); + // delay decref of the old value as lock-free type cache readers may access it + if (old_value != NULL && !_Py_IsImmortal(old_value)) { + _PyObject_XDecRefDelayed(old_value); + } return res; } diff --git a/Python/typecache.c b/Python/typecache.c index 870d6ce86297954..f346b0a48f702b7 100644 --- a/Python/typecache.c +++ b/Python/typecache.c @@ -1,8 +1,14 @@ -#include "Python.h" +// Lock-free per type method cache implementation. + +// The cache is used for method and attribute lookups on type objects. +// The stored names are always interned strings, and the +// stored values are borrowed references to the corresponding method or attribute object. +// For static types, the cache is stored on the per-interpreter managed_static_type_state, +// and for heap types the cache is stored in the `PyTypeObject._tp_cache` field. +#include "Python.h" #include "pycore_typecache.h" #include "pycore_interp.h" // PyInterpreterState -#include "pycore_object.h" // _PyObject_XDecRefDelayed() #include "pycore_pymem.h" #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_pyatomic_ft_wrappers.h" @@ -18,7 +24,10 @@ static struct { .used = 0, }, }; - +// The empty cache is statically allocated and shared across all the types, +// when a type is modified, the cache of type is set to the empty cache +// and when a cache entry is inserted to the empty cache, a new cache is +// allocated for the type and the entry is inserted to the new cache. #define empty_cache (empty_cache_storage.cache) static inline uint32_t cache_size(struct type_cache *cache) @@ -95,7 +104,10 @@ static inline void type_cache_insert(struct type_cache *cache, PyObject *name, uint32_t index = hash & cache->mask; for (;;) { if (cache->hashtable[index].name == NULL) { +#ifndef Py_GIL_DISABLED + // On free-threading, all interned strings are immortal. Py_INCREF(name); +#endif FT_ATOMIC_STORE_PTR(cache->hashtable[index].value, value); FT_ATOMIC_STORE_PTR(cache->hashtable[index].name, name); cache->used++; @@ -179,6 +191,7 @@ struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject } index = (index + 1) & cache->mask; } + // to maintain consistency with find_name_in_mro and prevent stale cache reads uint32_t cache_version = FT_ATOMIC_LOAD_UINT_RELAXED(cache->version_tag); if (cache_version != FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)) { PyStackRef_XCLOSE(out_ref); @@ -187,6 +200,7 @@ struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject return (struct _PyTypeCacheLookupResult){out_ref, 1, cache_version}; } + void _PyTypeCache_Invalidate(PyTypeObject *type) { struct type_cache *cache = get_cache(type); set_cache(type, &empty_cache); From 7416272906f9cde0c8f17e86c5f570f169cc6582 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 21 May 2026 22:04:27 +0530 Subject: [PATCH 06/11] consistent function naming --- Python/typecache.c | 48 +++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/Python/typecache.c b/Python/typecache.c index f346b0a48f702b7..edcfb0d2e9eca26 100644 --- a/Python/typecache.c +++ b/Python/typecache.c @@ -41,31 +41,36 @@ static inline size_t cache_nbytes(struct type_cache *cache) + (size_t)cache_size(cache) * sizeof(struct type_cache_entry); } -static struct type_cache *allocate_cache(uint32_t size) +static struct type_cache *cache_allocate(uint32_t size) { + // size must be a power of two assert((size & (size - 1)) == 0); struct type_cache *cache = PyMem_Calloc(1, sizeof(struct type_cache) + size * sizeof(struct type_cache_entry)); if (cache == NULL) { return NULL; } cache->mask = size - 1; + // load factor of 0.75 cache->available = size - (size >> 2); cache->used = 0; return cache; } -static void free_cache_delayed(struct type_cache *cache) +static void cache_free_delayed(struct type_cache *cache) { if (cache == NULL || cache == &empty_cache) { return; } #ifndef Py_GIL_DISABLED + // On gil-enabled builds, the cache owns strong references to the interned strings, + // so we need to decref them before freeing the cache memory. for (uint32_t i = 0; i < cache_size(cache); i++) { if (cache->hashtable[i].name != NULL) { Py_DECREF(cache->hashtable[i].name); } } #endif + // Delay the freeing of old cache for concurrent lock-free readers _PyMem_FreeDelayed(cache, cache_nbytes(cache)); } @@ -81,12 +86,12 @@ static inline void **cache_slot(PyTypeObject *type) return &type->_tp_cache; } -static inline struct type_cache *get_cache(PyTypeObject *type) +static inline struct type_cache *cache_get(PyTypeObject *type) { return (struct type_cache *)FT_ATOMIC_LOAD_PTR(*cache_slot(type)); } -static inline void set_cache(PyTypeObject *type, struct type_cache *cache) +static inline void cache_set(PyTypeObject *type, struct type_cache *cache) { FT_ATOMIC_STORE_PTR(*cache_slot(type), cache); } @@ -96,7 +101,7 @@ void _PyTypeCache_InitType(PyTypeObject *type) *cache_slot(type) = &empty_cache; } -static inline void type_cache_insert(struct type_cache *cache, PyObject *name, +static inline void cache_insert(struct type_cache *cache, PyObject *name, PyObject *value) { Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(name); @@ -115,49 +120,55 @@ static inline void type_cache_insert(struct type_cache *cache, PyObject *name, return; } else if (cache->hashtable[index].name == name) { + /* someone else added the entry before us. */ return; } index = (index + 1) & cache->mask; } } -static inline int type_cache_resize(PyTypeObject *type, struct type_cache *cache) +static inline int cache_resize(PyTypeObject *type, struct type_cache *cache) { uint32_t old_size = cache_size(cache); uint32_t new_size; if (cache->used == 0) { + // the cache is the empty cache, we need to allocate a new cache with the minimum size new_size = _Py_TYPECACHE_MINSIZE; } else { + // double the cache size when resizing new_size = old_size * 2; } - struct type_cache *new_cache = allocate_cache(new_size); + struct type_cache *new_cache = cache_allocate(new_size); if (new_cache == NULL) { return -1; } FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); for (uint32_t i = 0; i < old_size; i++) { if (cache->hashtable[i].name != NULL) { - type_cache_insert(new_cache, cache->hashtable[i].name, + cache_insert(new_cache, cache->hashtable[i].name, cache->hashtable[i].value); } } - set_cache(type, new_cache); - free_cache_delayed(cache); + cache_set(type, new_cache); + cache_free_delayed(cache); return 0; } void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) { - struct type_cache *cache = get_cache(type); + struct type_cache *cache = cache_get(type); + // If the cache is full, resize it before inserting the new entry. + // this also handles the case of empty cache where available is 0 but there are no entries. if (cache->available == 0) { - if (type_cache_resize(type, cache) == -1) { + if (cache_resize(type, cache) == -1) { + // out of memory, don't cache the value return; } - cache = get_cache(type); + cache = cache_get(type); assert(cache->available > 0); } - type_cache_insert(cache, name, value); + cache_insert(cache, name, value); FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); } @@ -165,7 +176,7 @@ struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject { assert(PyUnicode_CheckExact(name) && PyUnicode_CHECK_INTERNED(name)); struct _PyTypeCacheLookupResult miss = {PyStackRef_NULL, 0, 0}; - struct type_cache *cache = get_cache(type); + struct type_cache *cache = cache_get(type); if (cache == NULL) { return miss; } @@ -202,7 +213,8 @@ struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject void _PyTypeCache_Invalidate(PyTypeObject *type) { - struct type_cache *cache = get_cache(type); - set_cache(type, &empty_cache); - free_cache_delayed(cache); + struct type_cache *cache = cache_get(type); + // if the type was modified, the cache is set to the empty cache and the old cache is freed after a delay. + cache_set(type, &empty_cache); + cache_free_delayed(cache); } From 16ab4f98bcfcbcde5f03b34d69602f444fd917c3 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 21 May 2026 22:21:30 +0530 Subject: [PATCH 07/11] formatting --- Python/typecache.c | 46 +++++++++++++++++++--------- Tools/c-analyzer/cpython/ignored.tsv | 3 ++ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Python/typecache.c b/Python/typecache.c index edcfb0d2e9eca26..a37118badeab688 100644 --- a/Python/typecache.c +++ b/Python/typecache.c @@ -30,22 +30,27 @@ static struct { // allocated for the type and the entry is inserted to the new cache. #define empty_cache (empty_cache_storage.cache) -static inline uint32_t cache_size(struct type_cache *cache) +static inline uint32_t +cache_size(struct type_cache *cache) { return cache->mask + 1; } -static inline size_t cache_nbytes(struct type_cache *cache) +static inline size_t +cache_nbytes(struct type_cache *cache) { return sizeof(struct type_cache) + (size_t)cache_size(cache) * sizeof(struct type_cache_entry); } -static struct type_cache *cache_allocate(uint32_t size) +static struct type_cache * +cache_allocate(uint32_t size) { // size must be a power of two assert((size & (size - 1)) == 0); - struct type_cache *cache = PyMem_Calloc(1, sizeof(struct type_cache) + size * sizeof(struct type_cache_entry)); + size_t nbytes = sizeof(struct type_cache) + + (size_t)size * sizeof(struct type_cache_entry); + struct type_cache *cache = PyMem_Calloc(1, nbytes); if (cache == NULL) { return NULL; } @@ -56,7 +61,8 @@ static struct type_cache *cache_allocate(uint32_t size) return cache; } -static void cache_free_delayed(struct type_cache *cache) +static void +cache_free_delayed(struct type_cache *cache) { if (cache == NULL || cache == &empty_cache) { return; @@ -75,7 +81,8 @@ static void cache_free_delayed(struct type_cache *cache) } -static inline void **cache_slot(PyTypeObject *type) +static inline void ** +cache_slot(PyTypeObject *type) { if (type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) { PyInterpreterState *interp = _PyInterpreterState_GET(); @@ -86,23 +93,27 @@ static inline void **cache_slot(PyTypeObject *type) return &type->_tp_cache; } -static inline struct type_cache *cache_get(PyTypeObject *type) +static inline struct type_cache * +cache_get(PyTypeObject *type) { return (struct type_cache *)FT_ATOMIC_LOAD_PTR(*cache_slot(type)); } -static inline void cache_set(PyTypeObject *type, struct type_cache *cache) +static inline void +cache_set(PyTypeObject *type, struct type_cache *cache) { FT_ATOMIC_STORE_PTR(*cache_slot(type), cache); } -void _PyTypeCache_InitType(PyTypeObject *type) +void +_PyTypeCache_InitType(PyTypeObject *type) { *cache_slot(type) = &empty_cache; } -static inline void cache_insert(struct type_cache *cache, PyObject *name, - PyObject *value) +static inline void +cache_insert(struct type_cache *cache, PyObject *name, + PyObject *value) { Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(name); assert(hash != -1); @@ -127,7 +138,8 @@ static inline void cache_insert(struct type_cache *cache, PyObject *name, } } -static inline int cache_resize(PyTypeObject *type, struct type_cache *cache) +static inline int +cache_resize(PyTypeObject *type, struct type_cache *cache) { uint32_t old_size = cache_size(cache); uint32_t new_size; @@ -155,7 +167,8 @@ static inline int cache_resize(PyTypeObject *type, struct type_cache *cache) return 0; } -void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) +void +_PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) { struct type_cache *cache = cache_get(type); // If the cache is full, resize it before inserting the new entry. @@ -172,7 +185,8 @@ void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); } -struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name) +struct _PyTypeCacheLookupResult +_PyTypeCache_Lookup(PyTypeObject *type, PyObject *name) { assert(PyUnicode_CheckExact(name) && PyUnicode_CHECK_INTERNED(name)); struct _PyTypeCacheLookupResult miss = {PyStackRef_NULL, 0, 0}; @@ -212,7 +226,9 @@ struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject } -void _PyTypeCache_Invalidate(PyTypeObject *type) { +void +_PyTypeCache_Invalidate(PyTypeObject *type) +{ struct type_cache *cache = cache_get(type); // if the type was modified, the cache is set to the empty cache and the old cache is freed after a delay. cache_set(type, &empty_cache); diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index ddfb93a424c0185..6f09fc665e2c857 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -57,6 +57,9 @@ Python/pyhash.c - _Py_HashSecret - ## thread-safe hashtable (internal locks) Python/parking_lot.c - buckets - +## shared empty sentinel for the per-type method cache +Python/typecache.c - empty_cache_storage - + ## data needed for introspecting asyncio state from debuggers and profilers Modules/_asynciomodule.c - _Py_AsyncioDebug - From 76962340c2e9c790326d323caf62af351271d3bb Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 24 May 2026 14:00:41 +0530 Subject: [PATCH 08/11] add tests --- Include/internal/pycore_typecache.h | 4 +- Lib/test/test_free_threading/test_type.py | 49 ++++++- Lib/test/test_type_cache.py | 157 ++++++++++++++++++++++ Modules/Setup.stdlib.in | 2 +- Modules/_testinternalcapi.c | 3 + Modules/_testinternalcapi/parts.h | 1 + Modules/_testinternalcapi/typecache.c | 91 +++++++++++++ PCbuild/_testinternalcapi.vcxproj | 1 + PCbuild/_testinternalcapi.vcxproj.filters | 3 + 9 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 Modules/_testinternalcapi/typecache.c diff --git a/Include/internal/pycore_typecache.h b/Include/internal/pycore_typecache.h index da805ca33a1cb85..7aef22ab7be5cd5 100644 --- a/Include/internal/pycore_typecache.h +++ b/Include/internal/pycore_typecache.h @@ -35,8 +35,8 @@ struct _PyTypeCacheLookupResult { extern void _PyTypeCache_InitType(PyTypeObject *type); extern void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value); -extern struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name); -extern void _PyTypeCache_Invalidate(PyTypeObject *type); +PyAPI_FUNC(struct _PyTypeCacheLookupResult) _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name); +PyAPI_FUNC(void) _PyTypeCache_Invalidate(PyTypeObject *type); #ifdef __cplusplus } diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py index f7bacab00846b99..977a8095e2c4e21 100644 --- a/Lib/test/test_free_threading/test_type.py +++ b/Lib/test/test_free_threading/test_type.py @@ -5,7 +5,9 @@ from threading import Thread from unittest import TestCase -from test.support import threading_helper +from test.support import import_helper, threading_helper + +_testinternalcapi = import_helper.import_module("_testinternalcapi") @@ -178,6 +180,51 @@ def reader(): self.run_one(writer, reader) + def test_per_type_cache_concurrent_reads(self): + class C: + pass + + names = [f"attr_{i}" for i in range( + _testinternalcapi._Py_TYPECACHE_MINSIZE * 4)] + for name in names: + setattr(C, name, name) + # Prime the cache. + for name in names: + getattr(C, name) + + lookup = _testinternalcapi.type_cache_lookup + + def reader(): + for _ in range(500): + for name in names: + hit, value, _ = lookup(C, name) + self.assertEqual(hit, 1, name) + self.assertEqual(value, name) + + threading_helper.run_concurrently(reader, nthreads=NTHREADS) + + def test_per_type_cache_concurrent_invalidate(self): + class C: + x = "value" + + # Prime the cache. + C.x + hit, value, version = _testinternalcapi.type_cache_lookup(C, "x") + self.assertEqual(hit, 1) + self.assertIs(value, "value") + self.assertGreater(version, 0) + + def reader(): + for _ in range(10_000): + self.assertIs(C.x, "value") + + def invalidator(): + for _ in range(10_000): + _testinternalcapi.type_cache_invalidate(C) + + workers = [invalidator] + [reader] * (NTHREADS - 1) + threading_helper.run_concurrently(workers) + def run_one(self, writer_func, reader_func): barrier = threading.Barrier(NTHREADS) diff --git a/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py index 22ad9f6243eda91..8fc4f1b7a730e65 100644 --- a/Lib/test/test_type_cache.py +++ b/Lib/test/test_type_cache.py @@ -261,5 +261,162 @@ def to_bool_2(instance): self._check_specialization(to_bool_2, H(), "TO_BOOL", should_specialize=False) +@support.cpython_only +class PerTypeLookupCacheTests(unittest.TestCase): + """Tests for the per-type lookup cache.""" + + type_cache_lookup = staticmethod(_testinternalcapi.type_cache_lookup) + type_cache_invalidate = staticmethod(_testinternalcapi.type_cache_invalidate) + + def _make_type(self): + class C: + x = "x-value" + return C + + def test_lookup_miss_on_empty_cache(self): + # A freshly-created type has not cached any names yet; the cache + # should report a miss for an arbitrary name. + C = self._make_type() + hit, value, version = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 0) + self.assertIsNone(value) + self.assertEqual(version, 0) + + def test_lookup_hit_after_access(self): + # Reading an attribute goes through _PyType_Lookup which + # caches the result. Subsequent lookups for the same name + # should hit the cache. + C = self._make_type() + hit, value, version = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 0) + attr = C.x + hit, value, version = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 1) + self.assertIs(value, attr) + self.assertNotEqual(version, 0) + self.assertEqual(version, type_get_version(C)) + + def test_lookup_caches_missing_name(self): + # _PyType_Lookup caches negative results too: a name that is not in + # the MRO should still produce a cache hit with a None value. + C = self._make_type() + with self.assertRaises(AttributeError): + C.does_not_exist + hit, value, _ = self.type_cache_lookup(C, "does_not_exist") + self.assertEqual(hit, 1) + self.assertIsNone(value) + + def test_lookup_on_static_type(self): + # The cache for static types is stored on interpreter for isolation + # between subinterpreters, test that cache works for them as well. + self.type_cache_invalidate(int) + self.assertEqual(self.type_cache_lookup(int, "bit_length")[0], 0) + attr = int.bit_length + hit, value, _ = self.type_cache_lookup(int, "bit_length") + self.assertEqual(hit, 1) + self.assertIs(value, attr) + + def test_invalidate_clears_cache(self): + C = self._make_type() + C.x # populate cache + self.assertEqual(self.type_cache_lookup(C, "x")[0], 1) + + self.type_cache_invalidate(C) + hit, value, _ = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 0) + self.assertIsNone(value) + + def test_setattr_invalidates_cache(self): + # Mutating a type's attributes must invalidate any cached entries + # for that type. + C = self._make_type() + C.x + self.assertEqual(self.type_cache_lookup(C, "x")[0], 1) + + C.x = "new-value" + hit, _, _ = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 0) + + # The next access should re-populate the cache with the new value. + self.assertEqual(C.x, "new-value") + hit, value, _ = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 1) + self.assertEqual(value, "new-value") + + def test_setattr_on_subclass_preserves_base(self): + # Adding an attribute to a subclass changes the lookup result for + # the subclass, so its cache must be invalidated, but the base's + # cache for the same name stays valid. + class Base: + x = "base" + class Sub(Base): + pass + + self.assertEqual(Sub.x, "base") + self.assertEqual(Base.x, "base") + self.assertEqual(self.type_cache_lookup(Sub, "x")[0], 1) + self.assertEqual(self.type_cache_lookup(Base, "x")[0], 1) + + Sub.x = "sub" + # Sub's cache should be invalidated. + self.assertEqual(self.type_cache_lookup(Sub, "x")[0], 0) + # Base is untouched. + hit, value, _ = self.type_cache_lookup(Base, "x") + self.assertEqual(hit, 1) + self.assertEqual(value, "base") + + def test_setattr_on_base_invalidates_subclass(self): + class Base: + x = "base" + class Sub(Base): + pass + + Sub.x + self.assertEqual(self.type_cache_lookup(Sub, "x")[0], 1) + + Base.x = "new-base" + # Modifying the base must invalidate the subclass cache too. + self.assertEqual(self.type_cache_lookup(Sub, "x")[0], 0) + + def test_lookup_detects_stale_cache_version(self): + # The cache stores the type's tp_version_tag alongside its entries + # and re-checks it after locating a hit. If the type version moves + # forward without the cache being invalidated (the race window in + # lock-free invalidation), the consistency check must downgrade + # the hit to a miss. + C = self._make_type() + C.x # populate cache + orig_version = type_get_version(C) + self.assertNotEqual(orig_version, 0) + self.assertEqual(self.type_cache_lookup(C, "x")[0], 1) + + # Bump the type version directly without touching the cache slot + # (PyType_Modified would also invalidate, defeating the test). + type_assign_specific_version_unsafe(C, orig_version + 1) + self.assertEqual(type_get_version(C), orig_version + 1) + + hit, value, _ = self.type_cache_lookup(C, "x") + self.assertEqual(hit, 0) + self.assertIsNone(value) + + def test_setattr_on_unrelated_type_preserves_cache(self): + # Modifying one type must not invalidate a sibling's cache. + class A: + x = "a" + class B: + x = "b" + + A.x + B.x + self.assertEqual(self.type_cache_lookup(A, "x")[0], 1) + self.assertEqual(self.type_cache_lookup(B, "x")[0], 1) + + B.x = "b2" + # A's cache is unaffected. + hit, value, _ = self.type_cache_lookup(A, "x") + self.assertEqual(hit, 1) + self.assertEqual(value, "a") + + if __name__ == "__main__": unittest.main() diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index c3dd47a5e40a675..657b4cac43b4c6c 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -172,7 +172,7 @@ @MODULE_XXSUBTYPE_TRUE@xxsubtype xxsubtype.c @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c -@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c +@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index b8a22c439e853d9..df55ecc61fd604f 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -3345,6 +3345,9 @@ module_exec(PyObject *module) if (_PyTestInternalCapi_Init_Tuple(module) < 0) { return 1; } + if (_PyTestInternalCapi_Init_TypeCache(module) < 0) { + return 1; + } Py_ssize_t sizeof_gc_head = 0; #ifndef Py_GIL_DISABLED diff --git a/Modules/_testinternalcapi/parts.h b/Modules/_testinternalcapi/parts.h index 81f536c3babb18c..3775792b8b87156 100644 --- a/Modules/_testinternalcapi/parts.h +++ b/Modules/_testinternalcapi/parts.h @@ -16,5 +16,6 @@ int _PyTestInternalCapi_Init_Set(PyObject *module); int _PyTestInternalCapi_Init_Complex(PyObject *module); int _PyTestInternalCapi_Init_CriticalSection(PyObject *module); int _PyTestInternalCapi_Init_Tuple(PyObject *module); +int _PyTestInternalCapi_Init_TypeCache(PyObject *module); #endif // Py_TESTINTERNALCAPI_PARTS_H diff --git a/Modules/_testinternalcapi/typecache.c b/Modules/_testinternalcapi/typecache.c new file mode 100644 index 000000000000000..044eca35d56e633 --- /dev/null +++ b/Modules/_testinternalcapi/typecache.c @@ -0,0 +1,91 @@ +// Test wrappers for the per-type lookup cache (pycore_typecache.h). +// +// Insertion is exercised indirectly through normal attribute access (which +// calls _PyType_Lookup); only Lookup and Invalidate need direct wrappers. + +#include "parts.h" + +#include "pycore_stackref.h" // PyStackRef_AsPyObjectSteal() +#include "pycore_typecache.h" // _PyTypeCache_Lookup() + + +static int +require_type(PyObject *obj) +{ + if (!PyType_Check(obj)) { + PyErr_SetString(PyExc_TypeError, "expected a type"); + return -1; + } + return 0; +} + +static PyObject * +intern_name(PyObject *name) +{ + if (!PyUnicode_CheckExact(name)) { + PyErr_SetString(PyExc_TypeError, "name must be a str"); + return NULL; + } + Py_INCREF(name); + PyUnicode_InternInPlace(&name); + return name; +} + +// type_cache_lookup(type, name) -> (cache_hit, value_or_None, version_tag) +static PyObject * +type_cache_lookup(PyObject *Py_UNUSED(self), PyObject *args) +{ + PyObject *type_obj, *name; + if (!PyArg_ParseTuple(args, "OU", &type_obj, &name)) { + return NULL; + } + if (require_type(type_obj) < 0) { + return NULL; + } + name = intern_name(name); + if (name == NULL) { + return NULL; + } + struct _PyTypeCacheLookupResult r = + _PyTypeCache_Lookup((PyTypeObject *)type_obj, name); + Py_DECREF(name); + PyObject *value; + if (PyStackRef_IsNull(r.value)) { + value = Py_NewRef(Py_None); + } + else { + value = PyStackRef_AsPyObjectSteal(r.value); + } + return Py_BuildValue("(iNk)", + r.cache_hit, value, + (unsigned long)r.version_tag); +} + +static PyObject * +type_cache_invalidate(PyObject *Py_UNUSED(self), PyObject *type_obj) +{ + if (require_type(type_obj) < 0) { + return NULL; + } + _PyTypeCache_Invalidate((PyTypeObject *)type_obj); + Py_RETURN_NONE; +} + + +static PyMethodDef test_methods[] = { + {"type_cache_lookup", type_cache_lookup, METH_VARARGS}, + {"type_cache_invalidate", type_cache_invalidate, METH_O}, + {NULL}, +}; + +int +_PyTestInternalCapi_Init_TypeCache(PyObject *m) +{ + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } + if (PyModule_AddIntMacro(m, _Py_TYPECACHE_MINSIZE) < 0) { + return -1; + } + return 0; +} diff --git a/PCbuild/_testinternalcapi.vcxproj b/PCbuild/_testinternalcapi.vcxproj index f3e423fa04668ec..cd58c3523e8c160 100644 --- a/PCbuild/_testinternalcapi.vcxproj +++ b/PCbuild/_testinternalcapi.vcxproj @@ -101,6 +101,7 @@ + diff --git a/PCbuild/_testinternalcapi.vcxproj.filters b/PCbuild/_testinternalcapi.vcxproj.filters index 7ab242c2c230b67..4082c36234bdf0f 100644 --- a/PCbuild/_testinternalcapi.vcxproj.filters +++ b/PCbuild/_testinternalcapi.vcxproj.filters @@ -30,6 +30,9 @@ Source Files + + Source Files + From a9d69178548020e7da2be1f4cdcf592317ef8e98 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 24 May 2026 14:35:47 +0530 Subject: [PATCH 09/11] fix _PyTypeCache_Invalidate wrapper --- Modules/_testinternalcapi/typecache.c | 4 ++++ Objects/typeobject.c | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/_testinternalcapi/typecache.c b/Modules/_testinternalcapi/typecache.c index 044eca35d56e633..b5fe0cae353801b 100644 --- a/Modules/_testinternalcapi/typecache.c +++ b/Modules/_testinternalcapi/typecache.c @@ -5,6 +5,8 @@ #include "parts.h" +#include "pycore_critical_section.h" +#include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_stackref.h" // PyStackRef_AsPyObjectSteal() #include "pycore_typecache.h" // _PyTypeCache_Lookup() @@ -67,7 +69,9 @@ type_cache_invalidate(PyObject *Py_UNUSED(self), PyObject *type_obj) if (require_type(type_obj) < 0) { return NULL; } + Py_BEGIN_CRITICAL_SECTION_MUTEX(&_PyInterpreterState_GET()->types.mutex); _PyTypeCache_Invalidate((PyTypeObject *)type_obj); + Py_END_CRITICAL_SECTION(); Py_RETURN_NONE; } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 8305dd878642771..643db5bbd1a8e55 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6837,7 +6837,6 @@ type_dealloc(PyObject *self) Py_XDECREF(type->tp_bases); Py_XDECREF(type->tp_mro); Py_XDECREF(type->tp_cache); - _PyTypeCache_Invalidate(type); clear_tp_subclasses(type); /* A type's tp_doc is heap allocated, unlike the tp_doc slots From 8d24e309cad73ef06d8880030c1ae9832d514130 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 24 May 2026 14:57:52 +0530 Subject: [PATCH 10/11] add comments --- Include/internal/pycore_typecache.h | 22 ++++++++++++---------- Python/typecache.c | 13 ++++++++++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Include/internal/pycore_typecache.h b/Include/internal/pycore_typecache.h index 7aef22ab7be5cd5..b3a7a09a0181dd0 100644 --- a/Include/internal/pycore_typecache.h +++ b/Include/internal/pycore_typecache.h @@ -14,22 +14,24 @@ extern "C" { #define _Py_TYPECACHE_MINSIZE 8 struct type_cache_entry { - PyObject *name; - PyObject *value; + PyObject *name; // name of the attribute or method, interned string, NULL if the entry is empty + PyObject *value; // borrowed reference or NULL }; +// Per-type attribute lookup cache: speed up attribute and method lookups, +// see _PyTypeCache_Lookup(). struct type_cache { - uint32_t mask; - uint32_t version_tag; - uint32_t available; - uint32_t used; - struct type_cache_entry hashtable[1]; + uint32_t mask; // mask for indexing into hashtable, i.e. size of hashtable is mask + 1 + uint32_t version_tag; // initialized from type->tp_version_tag + uint32_t available; // number of available entries in hashtable + uint32_t used; // number of used entries in hashtable + struct type_cache_entry hashtable[1]; // hashtable entries, the total size is always power of 2 and at least _Py_TYPECACHE_MINSIZE }; struct _PyTypeCacheLookupResult { - _PyStackRef value; - int cache_hit; - uint32_t version_tag; + _PyStackRef value; // value is a stack reference to the cached attribute or method, or NULL if not found + int cache_hit; // 1 if the cache entry is valid and matches the type's version tag, 0 otherwise + uint32_t version_tag; // version tag of the type when the value was cached }; diff --git a/Python/typecache.c b/Python/typecache.c index a37118badeab688..67f086f2875afd3 100644 --- a/Python/typecache.c +++ b/Python/typecache.c @@ -155,18 +155,20 @@ cache_resize(PyTypeObject *type, struct type_cache *cache) if (new_cache == NULL) { return -1; } - FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); for (uint32_t i = 0; i < old_size; i++) { if (cache->hashtable[i].name != NULL) { cache_insert(new_cache, cache->hashtable[i].name, cache->hashtable[i].value); } } + new_cache->version_tag = cache->version_tag; cache_set(type, new_cache); cache_free_delayed(cache); return 0; } +// Insert a new entry to the type cache. If the cache is full, resize it before inserting the new entry. +// The TYPE_LOCK should be held while calling this function. void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) { @@ -185,6 +187,10 @@ _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value) FT_ATOMIC_STORE_UINT_RELAXED(cache->version_tag, FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)); } + +// Lookup the given name in the type cache. +// The cache is lock-free so it is possible that cache becomes stale during the lookup, +// to prevent returning stale cache entry, the cache version is compared with the type version tag. struct _PyTypeCacheLookupResult _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name) { @@ -225,12 +231,13 @@ _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name) return (struct _PyTypeCacheLookupResult){out_ref, 1, cache_version}; } - +// Invalidate the type cache of the type. +// The cache is set to the empty cache and the old cache is freed with QSBR. +// The TYPE_LOCK should be held while calling this function. void _PyTypeCache_Invalidate(PyTypeObject *type) { struct type_cache *cache = cache_get(type); - // if the type was modified, the cache is set to the empty cache and the old cache is freed after a delay. cache_set(type, &empty_cache); cache_free_delayed(cache); } From f62d1e98d3842de027edbcf39414503cc6134503 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 24 May 2026 16:32:35 +0530 Subject: [PATCH 11/11] fix tests for refleak check mode --- Lib/test/test_free_threading/test_type.py | 4 ++-- Lib/test/test_type_cache.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py index 977a8095e2c4e21..0516c30493a224c 100644 --- a/Lib/test/test_free_threading/test_type.py +++ b/Lib/test/test_free_threading/test_type.py @@ -4,7 +4,7 @@ from concurrent.futures import ThreadPoolExecutor from threading import Thread from unittest import TestCase - +import sys from test.support import import_helper, threading_helper _testinternalcapi = import_helper.import_module("_testinternalcapi") @@ -184,7 +184,7 @@ def test_per_type_cache_concurrent_reads(self): class C: pass - names = [f"attr_{i}" for i in range( + names = [sys.intern(f"attr_{i}") for i in range( _testinternalcapi._Py_TYPECACHE_MINSIZE * 4)] for name in names: setattr(C, name, name) diff --git a/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py index 8fc4f1b7a730e65..43304bee9561355 100644 --- a/Lib/test/test_type_cache.py +++ b/Lib/test/test_type_cache.py @@ -1,5 +1,6 @@ """ Tests for the internal type cache in CPython. """ import dis +import sys import unittest import warnings from test import support @@ -310,9 +311,10 @@ def test_lookup_on_static_type(self): # The cache for static types is stored on interpreter for isolation # between subinterpreters, test that cache works for them as well. self.type_cache_invalidate(int) - self.assertEqual(self.type_cache_lookup(int, "bit_length")[0], 0) - attr = int.bit_length - hit, value, _ = self.type_cache_lookup(int, "bit_length") + name = sys.intern("bit_length") + self.assertEqual(self.type_cache_lookup(int, name)[0], 0) + attr = getattr(int, name) + hit, value, _ = self.type_cache_lookup(int, name) self.assertEqual(hit, 1) self.assertIs(value, attr)