gh-145685: per-type method cache implementation (#150160)

kumaraditya303 <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/daf09e17bf99c544e8ab7a72b11f10f2e382a381
commit: daf09e17bf99c544e8ab7a72b11f10f2e382a381
branch: main
author: Kumar Aditya <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-07-21T14:11:22+05:30
summary:

gh-145685: per-type method cache implementation (#150160)

files:
A Include/internal/pycore_typecache.h
A Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-09-25-08.gh-issue-145685.afO33b.rst
A Modules/_testinternalcapi/typecache.c
A Python/typecache.c
M Doc/c-api/type.rst
M Doc/whatsnew/3.16.rst
M Include/cpython/object.h
M Include/cpython/pystats.h
M Include/internal/pycore_interp_structs.h
M Include/internal/pycore_object.h
M Include/internal/pycore_typeobject.h
M Lib/test/test_free_threading/test_type.py
M Lib/test/test_sys.py
M Lib/test/test_type_cache.py
M Makefile.pre.in
M Modules/Setup.stdlib.in
M Modules/_testinternalcapi.c
M Modules/_testinternalcapi/parts.h
M Objects/typeobject.c
M PCbuild/_freeze_module.vcxproj
M PCbuild/_freeze_module.vcxproj.filters
M PCbuild/_testinternalcapi.vcxproj
M PCbuild/_testinternalcapi.vcxproj.filters
M PCbuild/pythoncore.vcxproj
M PCbuild/pythoncore.vcxproj.filters
M Python/pystate.c
M Python/pystats.c
M Tools/c-analyzer/cpython/ignored.tsv
M Tools/ftscalingbench/ftscalingbench.py

diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst
index 99a7ed8b1cb7b8e..2e48c56bd2c5a5b 100644
--- a/Doc/c-api/type.rst
+++ b/Doc/c-api/type.rst
@@ -37,6 +37,10 @@ Type Objects
 
    Clear the internal lookup cache. Return the current version tag.
 
+   .. versionchanged:: 3.16
+      This function is now a no-op as the type cache is now implemented
+      per-type. It still returns the current version tag.
+
 .. c:function:: unsigned long PyType_GetFlags(PyTypeObject* type)
 
    Return the :c:member:`~PyTypeObject.tp_flags` member of *type*. This function is primarily
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 7aac17fc1c7b433..662defa709a246c 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -815,7 +815,8 @@ New features
 Porting to Python 3.16
 ----------------------
 
-* TODO
+* :c:func:`PyType_ClearCache` is now a no-op as the type cache is now
+  implemented per-type. It still returns the current version tag.
 
 Deprecated C APIs
 -----------------
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/cpython/pystats.h b/Include/cpython/pystats.h
index 69659c48a3bf882..738e342d1407686 100644
--- a/Include/cpython/pystats.h
+++ b/Include/cpython/pystats.h
@@ -96,7 +96,9 @@ typedef struct _object_stats {
     uint64_t type_cache_misses;
     uint64_t type_cache_dunder_hits;
     uint64_t type_cache_dunder_misses;
-    uint64_t type_cache_collisions;
+    uint64_t type_cache_too_big;
+    uint64_t type_cache_invalidations;
+    uint64_t type_cache_resizes;
     /* Temporary value used during GC */
     uint64_t object_visits;
 } ObjectStats;
diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h
index d3efac906aeb933..0623adce693d465 100644
--- a/Include/internal/pycore_interp_structs.h
+++ b/Include/internal/pycore_interp_structs.h
@@ -560,23 +560,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;
@@ -591,6 +574,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 */
@@ -601,8 +588,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 307ecdbaaefcea2..41786cb267c2e96 100644
--- a/Include/internal/pycore_object.h
+++ b/Include/internal/pycore_object.h
@@ -285,8 +285,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..76f87e8548d9c90
--- /dev/null
+++ b/Include/internal/pycore_typecache.h
@@ -0,0 +1,47 @@
+#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 (1 << 3)
+#define _Py_TYPECACHE_MAXSIZE (1 << 16)
+
+struct type_cache_entry {
+    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;                                              // mask for indexing into hashtable, i.e. size of hashtable is mask + 1
+    uint32_t available;                                         // number of available entries in hashtable
+    uint32_t used;                                              // number of used entries in hashtable
+    unsigned int version_tag;                                   // initialized from type->tp_version_tag
+    struct type_cache_entry hashtable[_Py_TYPECACHE_MINSIZE];   // hashtable entries
+};
+
+struct _PyTypeCacheLookupResult {
+    _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
+    unsigned int version_tag;   // version tag of the type when the value was cached
+};
+
+
+extern void _PyTypeCache_InitType(PyTypeObject *type);
+extern void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value);
+PyAPI_FUNC(struct _PyTypeCacheLookupResult) _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name);
+PyAPI_FUNC(void) _PyTypeCache_Invalidate(PyTypeObject *type);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* PY_INTERNAL_TYPECACHE_H */
diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h
index c8b888f1c395483..ecb3c78a35b9165 100644
--- a/Include/internal/pycore_typeobject.h
+++ b/Include/internal/pycore_typeobject.h
@@ -41,7 +41,6 @@ extern PyStatus _PyTypes_InitTypes(PyInterpreterState *);
 extern void _PyTypes_FiniTypes(PyInterpreterState *);
 extern void _PyTypes_FiniExtTypes(PyInterpreterState *interp);
 extern void _PyTypes_Fini(PyInterpreterState *);
-extern void _PyTypes_AfterFork(void);
 extern void _PyTypes_FiniCachedDescriptors(PyInterpreterState *);
 
 static inline PyObject **
diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py
index 0c99bbc759e672a..f17d12532c63ced 100644
--- a/Lib/test/test_free_threading/test_type.py
+++ b/Lib/test/test_free_threading/test_type.py
@@ -4,8 +4,10 @@
 from concurrent.futures import ThreadPoolExecutor
 from threading import Barrier, Thread
 from unittest import TestCase
+import sys
+from test.support import import_helper, threading_helper
 
-from test.support import threading_helper
+_testinternalcapi = import_helper.import_module("_testinternalcapi")
 
 
 
@@ -84,6 +86,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
 
@@ -160,6 +180,51 @@ def reader():
 
         self.run_one(writer, reader)
 
+    def test_per_type_cache_concurrent_reads(self):
+        class C:
+            pass
+
+        names = [sys.intern(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 test_race_type_attr_added(self):
         NROUNDS = 50
         NSTOPPERS = 4
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 56c5c2aa2025b9a..dab03ef06a8b8e5 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1809,7 +1809,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/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py
index 849a2afd8ed7986..9827f2498554a5b 100644
--- a/Lib/test/test_type_cache.py
+++ b/Lib/test/test_type_cache.py
@@ -1,6 +1,7 @@
 """ Tests for the internal type cache in CPython. """
 import collections.abc
 import dis
+import sys
 import unittest
 import warnings
 from test import support
@@ -281,5 +282,163 @@ def to_bool_2(instance):
         self._check_specialization(to_bool_2, H(), "TO_BOOL", should_specialize=False)
 
 
[email protected]_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)
+        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)
+
+    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/Makefile.pre.in b/Makefile.pre.in
index b42a8491733c636..465981f8fd4deda 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -511,6 +511,7 @@ PYTHON_OBJS=	\
 		Python/thread.o \
 		Python/traceback.o \
 		Python/tracemalloc.o \
+		Python/typecache.o \
 		Python/uniqueid.o \
 		Python/getopt.o \
 		Python/pystrcmp.o \
@@ -1419,6 +1420,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/Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-09-25-08.gh-issue-145685.afO33b.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-09-25-08.gh-issue-145685.afO33b.rst
new file mode 100644
index 000000000000000..c95fc34b546aabb
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-09-25-08.gh-issue-145685.afO33b.rst
@@ -0,0 +1 @@
+The internal method cache used for types is now a per-type cache instead of a global per-interpreter one, improving performance and reducing cache misses. Patch by Kumar Aditya.
diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in
index 524f1466b191f64..0f88dddf03b9f68 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 _testcapi/weakref.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 _testlimitedcapi/weakref.c _testlimitedcapi/run.c
 @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index ea3ad2b81c28668..166397a72da3aa7 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -3423,6 +3423,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..b5fe0cae353801b
--- /dev/null
+++ b/Modules/_testinternalcapi/typecache.c
@@ -0,0 +1,95 @@
+// 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_critical_section.h"
+#include "pycore_pystate.h"       // _PyInterpreterState_GET()
+#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;
+    }
+    Py_BEGIN_CRITICAL_SECTION_MUTEX(&_PyInterpreterState_GET()->types.mutex);
+    _PyTypeCache_Invalidate((PyTypeObject *)type_obj);
+    Py_END_CRITICAL_SECTION();
+    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/Objects/typeobject.c b/Objects/typeobject.c
index ffa20ccaf3dfb87..60e26eab069b094 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,17 @@ _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 +1160,7 @@ _PyType_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 +1244,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
@@ -6212,69 +6143,6 @@ 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.
    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
@@ -6305,45 +6173,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));
 
@@ -6354,14 +6193,25 @@ _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);
+        // find_name_in_mro can release the type lock and another thread can
+        // modify the type, so we need to check version tag again before caching the result.
+        if (res >= 0 && version_tag != 0 && version_tag == type->tp_version_tag) {
+            _PyTypeCache_Insert(type, name, PyStackRef_AsPyObjectBorrow(*out));
+        }
         END_TYPE_LOCK();
     }
     else {
+        version_tag = 0;
         res = find_name_in_mro(type, name, out);
     }
 
@@ -6371,17 +6221,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;
 }
 
@@ -6840,7 +6679,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;
 }
 
@@ -6912,6 +6754,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);
@@ -7022,6 +6865,7 @@ type_dealloc(PyObject *self)
     Py_XDECREF(type->tp_mro);
     Py_XDECREF(type->tp_cache);
     clear_tp_subclasses(type);
+    _PyTypeCache_Invalidate(type);
 
     /* A type's tp_doc is heap allocated, unlike the tp_doc slots
      * of most other objects.  It's okay to cast it to char *.
@@ -9536,6 +9380,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 04bd36404366a57..246a07785168161 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -284,6 +284,7 @@
     <ClCompile Include="..\Python\thread.c" />
     <ClCompile Include="..\Python\traceback.c" />
     <ClCompile Include="..\Python\tracemalloc.c" />
+    <ClCompile Include="..\Python\typecache.c" />
     <ClCompile Include="..\Python\uniqueid.c" />
   </ItemGroup>
   <ItemGroup>
diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters
index 7710acf43b5137c..35082f57f54e9d8 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -496,6 +496,9 @@
     <ClCompile Include="..\Python\tracemalloc.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\typecache.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\uniqueid.c">
       <Filter>Source Files</Filter>
     </ClCompile>
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 @@
     <ClCompile Include="..\Modules\_testinternalcapi\complex.c" />
     <ClCompile Include="..\Modules\_testinternalcapi\interpreter.c" />
     <ClCompile Include="..\Modules\_testinternalcapi\tuple.c" />
+    <ClCompile Include="..\Modules\_testinternalcapi\typecache.c" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\PC\python_nt.rc" />
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 @@
     <ClCompile Include="..\Modules\_testinternalcapi\tuple.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Modules\_testinternalcapi\typecache.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\PC\python_nt.rc">
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 47296a93424a8f6..7fec674c550e027 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -332,6 +332,7 @@
     <ClInclude Include="..\Include\internal\pycore_tracemalloc.h" />
     <ClInclude Include="..\Include\internal\pycore_tstate.h" />
     <ClInclude Include="..\Include\internal\pycore_tuple.h" />
+    <ClInclude Include="..\Include\internal\pycore_typecache.h" />
     <ClInclude Include="..\Include\internal\pycore_typedefs.h" />
     <ClInclude Include="..\Include\internal\pycore_typeobject.h" />
     <ClInclude Include="..\Include\internal\pycore_typevarobject.h" />
@@ -703,6 +704,7 @@
     <ClCompile Include="..\Python\thread.c" />
     <ClCompile Include="..\Python\traceback.c" />
     <ClCompile Include="..\Python\tracemalloc.c" />
+    <ClCompile Include="..\Python\typecache.c" />
     <ClCompile Include="..\Python\uniqueid.c" />
   </ItemGroup>
   <ItemGroup Condition="$(IncludeExternals)">
diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters
index 124f6c7bb90de6c..cb0f6fce86df51a 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -897,6 +897,9 @@
     <ClInclude Include="..\Include\internal\pycore_tuple.h">
       <Filter>Include\internal</Filter>
     </ClInclude>
+    <ClInclude Include="..\Include\internal\pycore_typecache.h">
+      <Filter>Include\internal</Filter>
+    </ClInclude>
     <ClInclude Include="..\Include\internal\pycore_typedefs.h">
       <Filter>Include\internal</Filter>
     </ClInclude>
@@ -1622,6 +1625,9 @@
     <ClCompile Include="..\Python\tracemalloc.c">
       <Filter>Python</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\typecache.c">
+      <Filter>Python</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\uniqueid.c">
       <Filter>Python</Filter>
     </ClCompile>
diff --git a/Python/pystate.c b/Python/pystate.c
index e90642fa882db72..d10b38def32911d 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(), _Py_ClearImmortal()
+#include "pycore_object.h"        // _Py_ClearImmortal()
 #include "pycore_obmalloc.h"      // _PyMem_obmalloc_state_on_heap()
 #include "pycore_opcode_utils.h"  // NUM_COMMON_CONSTANTS
 #include "pycore_optimizer.h"     // JIT_CLEANUP_THRESHOLD
@@ -420,8 +420,6 @@ _PyRuntimeState_ReInitThreads(_PyRuntimeState *runtime)
     }
 #endif
 
-    _PyTypes_AfterFork();
-
     _PyThread_AfterFork(&runtime->threads);
 
     return _PyStatus_OK();
@@ -573,7 +571,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/pystats.c b/Python/pystats.c
index 2fac2db1b738c7e..6862521f659f202 100644
--- a/Python/pystats.c
+++ b/Python/pystats.c
@@ -230,9 +230,11 @@ print_object_stats(FILE *out, ObjectStats *stats)
     fprintf(out, "Object materialize dict (str subclass): %" PRIu64 "\n", stats->dict_materialized_str_subclass);
     fprintf(out, "Object method cache hits: %" PRIu64 "\n", stats->type_cache_hits);
     fprintf(out, "Object method cache misses: %" PRIu64 "\n", stats->type_cache_misses);
-    fprintf(out, "Object method cache collisions: %" PRIu64 "\n", stats->type_cache_collisions);
+    fprintf(out, "Object method cache too big: %" PRIu64 "\n", stats->type_cache_too_big);
     fprintf(out, "Object method cache dunder hits: %" PRIu64 "\n", stats->type_cache_dunder_hits);
     fprintf(out, "Object method cache dunder misses: %" PRIu64 "\n", stats->type_cache_dunder_misses);
+    fprintf(out, "Object method cache invalidations: %" PRIu64 "\n", stats->type_cache_invalidations);
+    fprintf(out, "Object method cache resizes: %" PRIu64 "\n", stats->type_cache_resizes);
 }
 
 static void
@@ -439,7 +441,9 @@ merge_object_stats(ObjectStats *dest, const ObjectStats *src)
     dest->type_cache_misses += src->type_cache_misses;
     dest->type_cache_dunder_hits += src->type_cache_dunder_hits;
     dest->type_cache_dunder_misses += src->type_cache_dunder_misses;
-    dest->type_cache_collisions += src->type_cache_collisions;
+    dest->type_cache_too_big += src->type_cache_too_big;
+    dest->type_cache_invalidations += src->type_cache_invalidations;
+    dest->type_cache_resizes += src->type_cache_resizes;
     dest->object_visits += src->object_visits;
 }
 
diff --git a/Python/typecache.c b/Python/typecache.c
new file mode 100644
index 000000000000000..26079de5ce341f7
--- /dev/null
+++ b/Python/typecache.c
@@ -0,0 +1,246 @@
+// 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_pymem.h"
+#include "pycore_pystate.h"       // _PyInterpreterState_GET()
+#include "pycore_pyatomic_ft_wrappers.h"
+#include "pycore_typeobject.h"    // _PyStaticType_GetState()
+#include "pycore_stats.h"         // OBJECT_STAT_INC
+
+
+// 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.
+static struct type_cache empty_cache = {
+    .mask = _Py_TYPECACHE_MINSIZE - 1,
+    .version_tag = 0,
+    .available = 0,
+    .used = 0,
+};
+
+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 offsetof(struct type_cache, hashtable)
+        + (size_t)cache_size(cache) * sizeof(struct type_cache_entry);
+}
+
+static struct type_cache *
+cache_allocate(uint32_t size)
+{
+    // size must be a power of two
+    assert((size & (size - 1)) == 0);
+    size_t nbytes = offsetof(struct type_cache, hashtable)
+                    + (size_t)size * sizeof(struct type_cache_entry);
+    struct type_cache *cache = PyMem_Calloc(1, nbytes);
+    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
+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));
+}
+
+
+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 *
+cache_get(PyTypeObject *type)
+{
+    return (struct type_cache *)FT_ATOMIC_LOAD_PTR_ACQUIRE(*cache_slot(type));
+}
+
+static inline void
+cache_set(PyTypeObject *type, struct type_cache *cache)
+{
+    FT_ATOMIC_STORE_PTR_RELEASE(*cache_slot(type), cache);
+}
+
+void
+_PyTypeCache_InitType(PyTypeObject *type)
+{
+    *cache_slot(type) = &empty_cache;
+}
+
+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);
+    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
+            cache->hashtable[index].value = value;
+            FT_ATOMIC_STORE_PTR_RELEASE(cache->hashtable[index].name, name);
+            cache->used++;
+            cache->available--;
+            return;
+        }
+        else if (cache->hashtable[index].name == name) {
+            /* someone else added the entry before us. */
+            return;
+        }
+        index = (index + 1) & cache->mask;
+    }
+}
+
+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;
+    }
+    if (new_size > _Py_TYPECACHE_MAXSIZE) {
+        // The new size is too big, don't resize and just return.
+        OBJECT_STAT_INC(type_cache_too_big);
+        return -1;
+    }
+    struct type_cache *new_cache = cache_allocate(new_size);
+    if (new_cache == NULL) {
+        return -1;
+    }
+    OBJECT_STAT_INC(type_cache_resizes);
+    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 = type->tp_version_tag;
+    cache_set(type, new_cache);
+    cache_free_delayed(cache);
+    return 0;
+}
+
+// Insert a new entry to the type cache.
+// The TYPE_LOCK should be held while calling this function.
+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.
+    // this also handles the case of empty cache where available is 0 but there are no entries.
+    if (cache->available == 0) {
+        if (cache_resize(type, cache) == -1) {
+            // out of memory, don't cache the value
+            return;
+        }
+        cache = cache_get(type);
+        assert(cache->available > 0);
+    }
+    assert(cache->version_tag == type->tp_version_tag);
+    cache_insert(cache, name, value);
+}
+
+
+// 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)
+{
+    assert(PyUnicode_CheckExact(name) && PyUnicode_CHECK_INTERNED(name));
+    struct _PyTypeCacheLookupResult miss = {PyStackRef_NULL, 0, 0};
+    struct type_cache *cache = cache_get(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_ACQUIRE(cache->hashtable[index].name);
+        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;
+        }
+        else if (entry_name == NULL) {
+            return miss;
+        }
+        index = (index + 1) & cache->mask;
+    }
+    // Check the cache version against the type version tag to maintain
+    // consistency with find_name_in_mro and prevent stale cache reads
+    if (cache->version_tag != FT_ATOMIC_LOAD_UINT_RELAXED(type->tp_version_tag)) {
+        PyStackRef_XCLOSE(out_ref);
+        return miss;
+    }
+    return (struct _PyTypeCacheLookupResult){out_ref, 1, cache->version_tag};
+}
+
+// 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)
+{
+    OBJECT_STAT_INC(type_cache_invalidations);
+    struct type_cache *cache = cache_get(type);
+    cache_set(type, &empty_cache);
+    cache_free_delayed(cache);
+}
diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv
index 6e18593ad698570..ef314625d507d61 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	-
+
 ## data needed for introspecting asyncio state from debuggers and profilers
 Modules/_asynciomodule.c	-	_Py_AsyncioDebug	-
 
diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py
index c8a914c22a9e137..a79242e740371ba 100644
--- a/Tools/ftscalingbench/ftscalingbench.py
+++ b/Tools/ftscalingbench/ftscalingbench.py
@@ -325,6 +325,19 @@ def enum_attr():
         MyEnum.Y
         MyEnum.Z
 
+_MCACHE_NUM_TYPES = 1 << 14
+_MCACHE_PAIRS = [
+    (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
+    for _ in range(WORK_SCALE // 10):
+        for inst, name in pairs:
+            getattr(inst, name)
+
 
 def bench_one_thread(func):
     t0 = time.perf_counter_ns()

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.