Skip to content

Commit 9c1f7ba

Browse files
committed
mro: thread-safe MRO cache
1 parent 2a4c17e commit 9c1f7ba

18 files changed

Lines changed: 597 additions & 115 deletions

‎Include/cpython/object.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ typedef struct {
136136
* backwards-compatibility */
137137
typedef Py_ssize_t printfunc;
138138

139+
struct _Py_mro_cache_entry;
140+
141+
typedef struct {
142+
struct _Py_mro_cache_entry *buckets;
143+
uint32_t mask;
144+
} _Py_mro_cache;
145+
146+
139147
// If this structure is modified, Doc/includes/typestruct.h should be updated
140148
// as well.
141149
struct _typeobject {
@@ -221,6 +229,9 @@ struct _typeobject {
221229
destructor tp_finalize;
222230
vectorcallfunc tp_vectorcall;
223231

232+
/* Added in version 3.13 */
233+
_Py_mro_cache tp_mro_cache;
234+
224235
/* bitset of which type-watchers care about this type */
225236
char tp_watched;
226237
};

‎Include/internal/pycore_interp.h‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ extern "C" {
2323
#include "pycore_gc.h" // struct _gc_runtime_state
2424
#include "pycore_list.h" // struct _Py_list_state
2525
#include "pycore_llist.h" // struct llist_node
26+
#include "pycore_mrocache.h" // struct _mro_cache_state
2627
#include "pycore_global_objects.h" // struct _Py_interp_static_objects
2728
#include "pycore_pymem.h" // struct _mem_work
2829
#include "pycore_tuple.h" // struct _Py_tuple_state
@@ -78,8 +79,6 @@ typedef struct PyThreadStateImpl {
7879
struct brc_state brc;
7980

8081
struct qsbr *qsbr;
81-
82-
struct type_cache type_cache;
8382
} PyThreadStateImpl;
8483

8584

@@ -127,6 +126,7 @@ struct _is {
127126
struct _ceval_state ceval;
128127
struct _gc_runtime_state gc;
129128
struct _mem_state mem;
129+
struct _mro_cache_state mro_cache;
130130

131131
// sys.modules dictionary
132132
PyObject *modules;
@@ -211,6 +211,8 @@ struct _is {
211211
struct callable_cache callable_cache;
212212
PyCodeObject *interpreter_trampoline;
213213

214+
struct _Py_queue_head mro_buckets_to_free;
215+
214216
struct _Py_interp_cached_objects cached_objects;
215217
struct _Py_interp_static_objects static_objects;
216218

‎Include/internal/pycore_mrocache.h‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
// TODO(sgross): MRO cache or type cache?
12+
13+
typedef struct _Py_mro_cache_entry {
14+
PyObject *name; /* name (interned unicode; immortal) */
15+
uintptr_t value; /* resolved function (owned ref), or 0=not cached 1=not present */
16+
} _Py_mro_cache_entry;
17+
18+
typedef struct _Py_mro_cache_buckets {
19+
struct _Py_queue_node node;
20+
union {
21+
Py_ssize_t refcount;
22+
Py_ssize_t capacity;
23+
} u;
24+
uint32_t available; /* number of unused buckets */
25+
uint32_t used; /* number of used buckets */
26+
_Py_mro_cache_entry array[];
27+
} _Py_mro_cache_buckets;
28+
29+
/* Per-interpreter state */
30+
struct _mro_cache_state {
31+
_Py_mro_cache_buckets *empty_buckets;
32+
Py_ssize_t empty_buckets_capacity;
33+
};
34+
35+
typedef struct _Py_mro_cache_result {
36+
int hit;
37+
PyObject *value;
38+
} _Py_mro_cache_result;
39+
40+
extern PyStatus _Py_mro_cache_init(PyInterpreterState *interp);
41+
extern void _Py_mro_cache_fini(PyInterpreterState *interp);
42+
extern void _Py_mro_cache_init_type(PyTypeObject *type);
43+
extern void _Py_mro_cache_fini_type(PyTypeObject *type);
44+
extern int _Py_mro_cache_visit(_Py_mro_cache *cache, visitproc visit, void *arg);
45+
46+
extern void _Py_mro_cache_erase(_Py_mro_cache *cache);
47+
extern void _Py_mro_cache_insert(_Py_mro_cache *cache, PyObject *name, PyObject *value);
48+
extern void _Py_mro_process_freed_buckets(PyInterpreterState *interp);
49+
50+
extern PyObject *_Py_mro_cache_as_dict(_Py_mro_cache *cache);
51+
52+
static inline _Py_mro_cache_result
53+
_Py_mro_cache_make_result(uintptr_t *ptr)
54+
{
55+
uintptr_t value = _Py_atomic_load_uintptr_relaxed(ptr);
56+
return (_Py_mro_cache_result) {
57+
.hit = value != 0,
58+
.value = (PyObject *)(value & ~1),
59+
};
60+
}
61+
62+
static inline struct _Py_mro_cache_result
63+
_Py_mro_cache_lookup(_Py_mro_cache *cache, PyObject *name)
64+
{
65+
Py_hash_t hash = ((PyASCIIObject *)name)->hash;
66+
uint32_t mask = _Py_atomic_load_uint32(&cache->mask);
67+
_Py_mro_cache_entry *first = _Py_atomic_load_ptr_relaxed(&cache->buckets);
68+
69+
Py_ssize_t offset = hash & mask;
70+
_Py_mro_cache_entry *bucket = (_Py_mro_cache_entry *)((char *)first + offset);
71+
72+
PyObject *entry_name = _Py_atomic_load_ptr_relaxed(&bucket->name);
73+
if (_PY_LIKELY(entry_name == name)) {
74+
return _Py_mro_cache_make_result(&bucket->value);
75+
}
76+
77+
/* First loop */
78+
while (1) {
79+
if (entry_name == NULL) {
80+
return (_Py_mro_cache_result){0, NULL};
81+
}
82+
if (bucket == first) {
83+
break;
84+
}
85+
bucket--;
86+
entry_name = _Py_atomic_load_ptr_relaxed(&bucket->name);
87+
if (entry_name == name) {
88+
return _Py_mro_cache_make_result(&bucket->value);
89+
}
90+
}
91+
92+
/* Second loop. Start at the last bucket. */
93+
bucket = (_Py_mro_cache_entry *)((char *)first + mask);
94+
while (1) {
95+
entry_name = _Py_atomic_load_ptr_relaxed(&bucket->name);
96+
if (entry_name == name) {
97+
return _Py_mro_cache_make_result(&bucket->value);
98+
}
99+
if (entry_name == NULL || bucket == first) {
100+
return (_Py_mro_cache_result){0, NULL};
101+
}
102+
bucket--;
103+
}
104+
}
105+
106+
107+
#ifdef __cplusplus
108+
}
109+
#endif
110+
#endif /* !Py_INTERNAL_TYPECACHE_H */

‎Include/internal/pycore_pymem.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ PyAPI_FUNC(int) _PyMem_SetupAllocators(PyMemAllocatorName allocator);
101101

102102
/* Free the pointer after all threads are quiescent. */
103103
extern void _PyMem_FreeQsbr(void *ptr);
104+
extern void _PyQsbr_Free(void *ptr, freefunc func);
104105
extern void _PyMem_QsbrPoll(PyThreadState *tstate);
105106
extern void _PyMem_AbandonQsbr(PyThreadState *tstate);
106107
extern void _PyMem_QsbrFini(PyInterpreterState *interp);

‎Include/internal/pycore_pyqueue.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ extern "C" {
1313
// struct _Py_queue_head which contains pointers to the first and
1414
// last node in the queue.
1515

16+
#define _Py_QUEUE_INIT(name) { { NULL }, &name.first }
17+
1618
static inline void
1719
_Py_queue_init(struct _Py_queue_head *head)
1820
{

‎Include/internal/pycore_typeobject.h‎

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,6 @@ extern void _PyTypes_Fini(PyInterpreterState *);
2626

2727
typedef struct wrapperbase pytype_slotdef;
2828

29-
30-
// Type attribute lookup cache: speed up attribute and method lookups,
31-
// see _PyType_Lookup().
32-
struct type_cache_entry {
33-
unsigned int version; // initialized from type->tp_version_tag
34-
PyObject *name; // reference to exactly a str or None
35-
PyObject *value; // borrowed reference or NULL
36-
};
37-
38-
#define MCACHE_SIZE_EXP 12
39-
40-
struct type_cache {
41-
struct type_cache_entry hashtable[1 << MCACHE_SIZE_EXP];
42-
};
43-
4429
/* For now we hard-code this to a value for which we are confident
4530
all the static builtin types will fit (for all builds). */
4631
#define _Py_MAX_STATIC_BUILTIN_TYPES 200
@@ -63,9 +48,6 @@ _PyStaticType_GET_WEAKREFS_LISTPTR(static_builtin_state *state)
6348
}
6449

6550
struct types_state {
66-
#ifndef Py_NOGIL
67-
struct type_cache type_cache;
68-
#endif
6951
size_t num_builtins_initialized;
7052
static_builtin_state builtins[_Py_MAX_STATIC_BUILTIN_TYPES];
7153
};

‎Makefile.pre.in‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ PYTHON_OBJS= \
400400
Python/lock.o \
401401
Python/marshal.o \
402402
Python/modsupport.o \
403+
Python/mrocache.o \
403404
Python/mysnprintf.o \
404405
Python/mystrtoul.o \
405406
Python/parking_lot.o \
@@ -1693,6 +1694,7 @@ PYTHON_HEADERS= \
16931694
$(srcdir)/Include/internal/pycore_list.h \
16941695
$(srcdir)/Include/internal/pycore_long.h \
16951696
$(srcdir)/Include/internal/pycore_moduleobject.h \
1697+
$(srcdir)/Include/internal/pycore_mrocache.h \
16961698
$(srcdir)/Include/internal/pycore_namespace.h \
16971699
$(srcdir)/Include/internal/pycore_object.h \
16981700
$(srcdir)/Include/internal/pycore_obmalloc.h \

‎Modules/_testbuffer.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2825,6 +2825,9 @@ PyInit__testbuffer(void)
28252825
{
28262826
PyObject *m;
28272827

2828+
if (PyType_Ready(&NDArray_Type) < 0)
2829+
return NULL;
2830+
28282831
m = PyModule_Create(&_testbuffermodule);
28292832
if (m == NULL)
28302833
return NULL;

‎Modules/gcmodule.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "pycore_pymem.h"
3434
#include "pycore_pystate.h"
3535
#include "pycore_refcnt.h"
36+
#include "pycore_qsbr.h"
3637
#include "pycore_gc.h"
3738
#include "frameobject.h" /* for PyFrame_ClearFreeList */
3839
#include "pydtrace.h"
@@ -1697,6 +1698,10 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
16971698
*/
16981699
handle_legacy_finalizers(tstate, gcstate, &finalizers);
16991700

1701+
_Py_qsbr_advance(&_PyRuntime.qsbr_shared);
1702+
_Py_qsbr_quiescent_state(tstate);
1703+
_PyMem_QsbrPoll(tstate);
1704+
17001705
if (_PyErr_Occurred(tstate)) {
17011706
if (reason == GC_REASON_SHUTDOWN) {
17021707
_PyErr_Clear(tstate);

0 commit comments

Comments
 (0)