Skip to content

Commit daf09e1

Browse files
gh-145685: per-type method cache implementation (#150160)
1 parent 202c4c8 commit daf09e1

29 files changed

Lines changed: 709 additions & 220 deletions

Doc/c-api/type.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Type Objects
3737
3838
Clear the internal lookup cache. Return the current version tag.
3939
40+
.. versionchanged:: 3.16
41+
This function is now a no-op as the type cache is now implemented
42+
per-type. It still returns the current version tag.
43+
4044
.. c:function:: unsigned long PyType_GetFlags(PyTypeObject* type)
4145
4246
Return the :c:member:`~PyTypeObject.tp_flags` member of *type*. This function is primarily

Doc/whatsnew/3.16.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,8 @@ New features
815815
Porting to Python 3.16
816816
----------------------
817817

818-
* TODO
818+
* :c:func:`PyType_ClearCache` is now a no-op as the type cache is now
819+
implemented per-type. It still returns the current version tag.
819820

820821
Deprecated C APIs
821822
-----------------

Include/cpython/object.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ struct _typeobject {
246246
* This function must escape to any code that can result in
247247
* the GC being run, such as Py_DECREF. */
248248
_Py_iteritemfunc _tp_iteritem;
249+
250+
void *_tp_cache;
249251
};
250252

251253
#define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used)

Include/cpython/pystats.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ typedef struct _object_stats {
9696
uint64_t type_cache_misses;
9797
uint64_t type_cache_dunder_hits;
9898
uint64_t type_cache_dunder_misses;
99-
uint64_t type_cache_collisions;
99+
uint64_t type_cache_too_big;
100+
uint64_t type_cache_invalidations;
101+
uint64_t type_cache_resizes;
100102
/* Temporary value used during GC */
101103
uint64_t object_visits;
102104
} ObjectStats;

Include/internal/pycore_interp_structs.h

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -560,23 +560,6 @@ struct _types_runtime_state {
560560
};
561561

562562

563-
// Type attribute lookup cache: speed up attribute and method lookups,
564-
// see _PyType_Lookup().
565-
struct type_cache_entry {
566-
unsigned int version; // initialized from type->tp_version_tag
567-
#ifdef Py_GIL_DISABLED
568-
_PySeqLock sequence;
569-
#endif
570-
PyObject *name; // reference to exactly a str or None
571-
PyObject *value; // borrowed reference or NULL
572-
};
573-
574-
#define MCACHE_SIZE_EXP 12
575-
576-
struct type_cache {
577-
struct type_cache_entry hashtable[1 << MCACHE_SIZE_EXP];
578-
};
579-
580563
typedef struct {
581564
PyTypeObject *type;
582565
int isbuiltin;
@@ -591,6 +574,10 @@ typedef struct {
591574
are also some diagnostic uses for the list of weakrefs,
592575
so we still keep it. */
593576
PyObject *tp_weaklist;
577+
/* Per-interpreter attribute lookup cache (struct type_cache *).
578+
For static builtin types the cache must be per-interpreter
579+
because tp_dict and the values it stores are per-interpreter. */
580+
void *_tp_cache;
594581
} managed_static_type_state;
595582

596583
#define TYPE_VERSION_CACHE_SIZE (1<<12) /* Must be a power of 2 */
@@ -601,8 +588,6 @@ struct types_state {
601588
where all those lower numbers are used for core static types. */
602589
unsigned int next_version_tag;
603590

604-
struct type_cache type_cache;
605-
606591
/* Every static builtin type is initialized for each interpreter
607592
during its own initialization, including for the main interpreter
608593
during global runtime initialization. This is done by calling

Include/internal/pycore_object.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,6 @@ _PyType_HasFeature(PyTypeObject *type, unsigned long feature) {
285285
return ((type->tp_flags) & feature) != 0;
286286
}
287287

288-
extern void _PyType_InitCache(PyInterpreterState *interp);
289-
290288
extern PyStatus _PyObject_InitState(PyInterpreterState *interp);
291289
extern void _PyObject_FiniState(PyInterpreterState *interp);
292290
extern bool _PyRefchain_IsTraced(PyInterpreterState *interp, PyObject *obj);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#ifndef PY_INTERNAL_TYPECACHE_H
2+
#define PY_INTERNAL_TYPECACHE_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#ifndef Py_BUILD_CORE
8+
# error "this header requires Py_BUILD_CORE define"
9+
#endif
10+
11+
#include "pycore_stackref.h"
12+
13+
14+
#define _Py_TYPECACHE_MINSIZE (1 << 3)
15+
#define _Py_TYPECACHE_MAXSIZE (1 << 16)
16+
17+
struct type_cache_entry {
18+
PyObject *name; // name of the attribute or method, interned string, NULL if the entry is empty
19+
PyObject *value; // borrowed reference or NULL
20+
};
21+
22+
// Per-type attribute lookup cache: speed up attribute and method lookups,
23+
// see _PyTypeCache_Lookup().
24+
struct type_cache {
25+
uint32_t mask; // mask for indexing into hashtable, i.e. size of hashtable is mask + 1
26+
uint32_t available; // number of available entries in hashtable
27+
uint32_t used; // number of used entries in hashtable
28+
unsigned int version_tag; // initialized from type->tp_version_tag
29+
struct type_cache_entry hashtable[_Py_TYPECACHE_MINSIZE]; // hashtable entries
30+
};
31+
32+
struct _PyTypeCacheLookupResult {
33+
_PyStackRef value; // value is a stack reference to the cached attribute or method, or NULL if not found
34+
int cache_hit; // 1 if the cache entry is valid and matches the type's version tag, 0 otherwise
35+
unsigned int version_tag; // version tag of the type when the value was cached
36+
};
37+
38+
39+
extern void _PyTypeCache_InitType(PyTypeObject *type);
40+
extern void _PyTypeCache_Insert(PyTypeObject *type, PyObject *name, PyObject *value);
41+
PyAPI_FUNC(struct _PyTypeCacheLookupResult) _PyTypeCache_Lookup(PyTypeObject *type, PyObject *name);
42+
PyAPI_FUNC(void) _PyTypeCache_Invalidate(PyTypeObject *type);
43+
44+
#ifdef __cplusplus
45+
}
46+
#endif
47+
#endif /* PY_INTERNAL_TYPECACHE_H */

Include/internal/pycore_typeobject.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ extern PyStatus _PyTypes_InitTypes(PyInterpreterState *);
4141
extern void _PyTypes_FiniTypes(PyInterpreterState *);
4242
extern void _PyTypes_FiniExtTypes(PyInterpreterState *interp);
4343
extern void _PyTypes_Fini(PyInterpreterState *);
44-
extern void _PyTypes_AfterFork(void);
4544
extern void _PyTypes_FiniCachedDescriptors(PyInterpreterState *);
4645

4746
static inline PyObject **

Lib/test/test_free_threading/test_type.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from concurrent.futures import ThreadPoolExecutor
55
from threading import Barrier, Thread
66
from unittest import TestCase
7+
import sys
8+
from test.support import import_helper, threading_helper
79

8-
from test.support import threading_helper
10+
_testinternalcapi = import_helper.import_module("_testinternalcapi")
911

1012

1113

@@ -84,6 +86,24 @@ def reader_func():
8486

8587
self.run_one(writer_func, reader_func)
8688

89+
def test_attr_cache_mortal(self):
90+
class C:
91+
x = object()
92+
93+
class D(C):
94+
pass
95+
96+
def writer_func():
97+
for _ in range(3000):
98+
C.x = object()
99+
100+
def reader_func():
101+
for _ in range(3000):
102+
C.x
103+
D.x
104+
105+
self.run_one(writer_func, reader_func)
106+
87107
def test___class___modification(self):
88108
loops = 200
89109

@@ -160,6 +180,51 @@ def reader():
160180

161181
self.run_one(writer, reader)
162182

183+
def test_per_type_cache_concurrent_reads(self):
184+
class C:
185+
pass
186+
187+
names = [sys.intern(f"attr_{i}") for i in range(
188+
_testinternalcapi._Py_TYPECACHE_MINSIZE * 4)]
189+
for name in names:
190+
setattr(C, name, name)
191+
# Prime the cache.
192+
for name in names:
193+
getattr(C, name)
194+
195+
lookup = _testinternalcapi.type_cache_lookup
196+
197+
def reader():
198+
for _ in range(500):
199+
for name in names:
200+
hit, value, _ = lookup(C, name)
201+
self.assertEqual(hit, 1, name)
202+
self.assertEqual(value, name)
203+
204+
threading_helper.run_concurrently(reader, nthreads=NTHREADS)
205+
206+
def test_per_type_cache_concurrent_invalidate(self):
207+
class C:
208+
x = "value"
209+
210+
# Prime the cache.
211+
C.x
212+
hit, value, version = _testinternalcapi.type_cache_lookup(C, "x")
213+
self.assertEqual(hit, 1)
214+
self.assertIs(value, "value")
215+
self.assertGreater(version, 0)
216+
217+
def reader():
218+
for _ in range(10_000):
219+
self.assertIs(C.x, "value")
220+
221+
def invalidator():
222+
for _ in range(10_000):
223+
_testinternalcapi.type_cache_invalidate(C)
224+
225+
workers = [invalidator] + [reader] * (NTHREADS - 1)
226+
threading_helper.run_concurrently(workers)
227+
163228
def test_race_type_attr_added(self):
164229
NROUNDS = 50
165230
NSTOPPERS = 4

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,7 @@ def delx(self): del self.__x
18091809
check((1,2,3), vsize('') + self.P + 3*self.P)
18101810
# type
18111811
# static type: PyTypeObject
1812-
fmt = 'P2nPI13Pl4Pn9Pn12PI2Pc'
1812+
fmt = 'P2nPI13Pl4Pn9Pn12PI2PcP'
18131813
s = vsize(fmt)
18141814
check(int, s)
18151815
typeid = 'n' if support.Py_GIL_DISABLED else ''

0 commit comments

Comments
 (0)