]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145685: per-type method cache implementation (#150160)
authorKumar Aditya <kumaraditya@python.org>
Tue, 21 Jul 2026 08:41:22 +0000 (14:11 +0530)
committerGitHub <noreply@github.com>
Tue, 21 Jul 2026 08:41:22 +0000 (14:11 +0530)
29 files changed:
Doc/c-api/type.rst
Doc/whatsnew/3.16.rst
Include/cpython/object.h
Include/cpython/pystats.h
Include/internal/pycore_interp_structs.h
Include/internal/pycore_object.h
Include/internal/pycore_typecache.h [new file with mode: 0644]
Include/internal/pycore_typeobject.h
Lib/test/test_free_threading/test_type.py
Lib/test/test_sys.py
Lib/test/test_type_cache.py
Makefile.pre.in
Misc/NEWS.d/next/Core_and_Builtins/2026-06-05-09-25-08.gh-issue-145685.afO33b.rst [new file with mode: 0644]
Modules/Setup.stdlib.in
Modules/_testinternalcapi.c
Modules/_testinternalcapi/parts.h
Modules/_testinternalcapi/typecache.c [new file with mode: 0644]
Objects/typeobject.c
PCbuild/_freeze_module.vcxproj
PCbuild/_freeze_module.vcxproj.filters
PCbuild/_testinternalcapi.vcxproj
PCbuild/_testinternalcapi.vcxproj.filters
PCbuild/pythoncore.vcxproj
PCbuild/pythoncore.vcxproj.filters
Python/pystate.c
Python/pystats.c
Python/typecache.c [new file with mode: 0644]
Tools/c-analyzer/cpython/ignored.tsv
Tools/ftscalingbench/ftscalingbench.py

index 99a7ed8b1cb7b8e6ef1caef3c39d334b7ef9409e..2e48c56bd2c5a5ba96a4b8a0f8bb669fab35b893 100644 (file)
@@ -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
index 7aac17fc1c7b433be33211a63c492cdeba42168b..662defa709a246c8fd731f9a3b07180598a4096d 100644 (file)
@@ -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
 -----------------
index 326254c335b4895763f655d2fa67e506e27c4d23..4c5a677e5543eceddf257124ed3ab9a49853dca1 100644 (file)
@@ -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)
index 69659c48a3bf88276f9d1c02d9b9d57cccde1c20..738e342d14076866ce63d332f7a88e65cb922615 100644 (file)
@@ -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;
index d3efac906aeb9333b5838b1bd006f7cd4ace714b..0623adce693d4650a33f0f00d9972bc972ad3016 100644 (file)
@@ -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
index 307ecdbaaefcea2618273b880abc0efcc8a2a857..41786cb267c2e96635c552faa19b7f2ad23494ee 100644 (file)
@@ -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 (file)
index 0000000..76f87e8
--- /dev/null
@@ -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 */
index c8b888f1c395483975175152d2270efaacbd3ca4..ecb3c78a35b91658f3a37488ee058ab290e32cb8 100644 (file)
@@ -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 **
index 0c99bbc759e672a34fc9618ccdc571c2dbfae990..f17d12532c63ced7e62c05e9741fbd06f2c95c34 100644 (file)
@@ -4,8 +4,10 @@ import unittest
 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 @@ class TestType(TestCase):
 
         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 @@ class TestType(TestCase):
 
         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
index 56c5c2aa2025b9a30075ac73e888dfaad3eb8a73..dab03ef06a8b8e5098ac8e8332aa01b2e9459576 100644 (file)
@@ -1809,7 +1809,7 @@ class SizeofTest(unittest.TestCase):
         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 ''
index 849a2afd8ed79863960a526916d2896df7d20120..9827f2498554a5bf6e7ce31de749e400c2a17027 100644 (file)
@@ -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 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
         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)
+        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()
index b42a8491733c636e20934df287acab6a7ffefcbb..465981f8fd4deda1234842a38f52e57326ad7a32 100644 (file)
@@ -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 (file)
index 0000000..c95fc34
--- /dev/null
@@ -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.
index 524f1466b191f64434624b40437bb24f6ecce096..0f88dddf03b9f68382164bafdb45355dd0cf6c76 100644 (file)
 @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
index ea3ad2b81c28668d9d345acf098e9231566ae114..166397a72da3aa70d71b5466758fc4d1e536e66a 100644 (file)
@@ -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
index 81f536c3babb18cd9c543b4d24e7008d4aa0a6ab..3775792b8b871564165a4082c1f8a3a84feb3cd1 100644 (file)
@@ -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 (file)
index 0000000..b5fe0ca
--- /dev/null
@@ -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;
+}
index ffa20ccaf3dfb87195a197f14504e39e1dcd9a68..60e26eab069b094ac0e20ec79202de803d103d60 100644 (file)
@@ -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
index 04bd36404366a57aa8eb1bea3961c743c86835d1..246a07785168161db62b99e97eb31b63a4dfee70 100644 (file)
     <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>
index 7710acf43b5137ce033d2525d67881c15a541245..35082f57f54e9d8c6f1854e2bd23b048dd97e66e 100644 (file)
     <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>
index f3e423fa04668ecc184a2a12b17f6516de32bfff..cd58c3523e8c160ef768264e210bc5e8dc6591d3 100644 (file)
     <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" />
index 7ab242c2c230b67e20a33d87cf6ed31c837d1ea4..4082c36234bdf0f8acc914dd5bb925a20607cd58 100644 (file)
@@ -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">
index 47296a93424a8f6651d672d795248615ed5d090c..7fec674c550e02761a0a906ce1cd3e9b511a060d 100644 (file)
     <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" />
     <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)">
index 124f6c7bb90de6c56144b143a347711fe63f77f0..cb0f6fce86df51a26403463e2aa082111da77313 100644 (file)
     <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>
     <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>
index e90642fa882db722710b9c4e60608b1964b1293a..d10b38def32911d2bab0e78209fe2ec753fa004b 100644 (file)
@@ -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
index 2fac2db1b738c7e04b3ba579828c53e30959863c..6862521f659f2028490793110c93a15c0bb29a3f 100644 (file)
@@ -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 (file)
index 0000000..26079de
--- /dev/null
@@ -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);
+}
index 6e18593ad698570de4a2581de08c931eb1751790..ef314625d507d61329df8f7005685b182b7d30c3 100644 (file)
@@ -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        -
 
index c8a914c22a9e137a3e1e98d6edc572127483d49d..a79242e740371badad87626adf98b24fcf367702 100644 (file)
@@ -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()