Skip to content

Commit 9b0bfba

Browse files
authored
gh-124218: Use per-thread reference counting for globals and builtins (#125713)
Use per-thread refcounting for the reference from function objects to the globals and builtins dictionaries.
1 parent d880c83 commit 9b0bfba

File tree

8 files changed

+115
-11
lines changed

8 files changed

+115
-11
lines changed

Include/cpython/dictobject.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ typedef struct {
1717
/* This is a private field for CPython's internal use.
1818
* Bits 0-7 are for dict watchers.
1919
* Bits 8-11 are for the watched mutation counter (used by tier2 optimization)
20-
* The remaining bits are not currently used. */
20+
* Bits 12-31 are currently unused
21+
* Bits 32-63 are a unique id in the free threading build (used for per-thread refcounting)
22+
*/
2123
uint64_t _ma_watcher_tag;
2224

2325
PyDictKeysObject *ma_keys;

Include/internal/pycore_dict.h

+34
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ static inline PyDictUnicodeEntry* DK_UNICODE_ENTRIES(PyDictKeysObject *dk) {
229229
#define DICT_VERSION_INCREMENT (1 << (DICT_MAX_WATCHERS + DICT_WATCHED_MUTATION_BITS))
230230
#define DICT_WATCHER_MASK ((1 << DICT_MAX_WATCHERS) - 1)
231231
#define DICT_WATCHER_AND_MODIFICATION_MASK ((1 << (DICT_MAX_WATCHERS + DICT_WATCHED_MUTATION_BITS)) - 1)
232+
#define DICT_UNIQUE_ID_SHIFT (32)
233+
#define DICT_UNIQUE_ID_MAX ((UINT64_C(1) << (64 - DICT_UNIQUE_ID_SHIFT)) - 1)
232234

233235

234236
PyAPI_FUNC(void)
@@ -307,8 +309,40 @@ _PyInlineValuesSize(PyTypeObject *tp)
307309
int
308310
_PyDict_DetachFromObject(PyDictObject *dict, PyObject *obj);
309311

312+
// Enables per-thread ref counting on this dict in the free threading build
313+
extern void _PyDict_EnablePerThreadRefcounting(PyObject *op);
314+
310315
PyDictObject *_PyObject_MaterializeManagedDict_LockHeld(PyObject *);
311316

317+
// See `_Py_INCREF_TYPE()` in pycore_object.h
318+
#ifndef Py_GIL_DISABLED
319+
# define _Py_INCREF_DICT Py_INCREF
320+
# define _Py_DECREF_DICT Py_DECREF
321+
#else
322+
static inline Py_ssize_t
323+
_PyDict_UniqueId(PyDictObject *mp)
324+
{
325+
// Offset by one so that _ma_watcher_tag=0 represents an unassigned id
326+
return (Py_ssize_t)(mp->_ma_watcher_tag >> DICT_UNIQUE_ID_SHIFT) - 1;
327+
}
328+
329+
static inline void
330+
_Py_INCREF_DICT(PyObject *op)
331+
{
332+
assert(PyDict_Check(op));
333+
Py_ssize_t id = _PyDict_UniqueId((PyDictObject *)op);
334+
_Py_THREAD_INCREF_OBJECT(op, id);
335+
}
336+
337+
static inline void
338+
_Py_DECREF_DICT(PyObject *op)
339+
{
340+
assert(PyDict_Check(op));
341+
Py_ssize_t id = _PyDict_UniqueId((PyDictObject *)op);
342+
_Py_THREAD_DECREF_OBJECT(op, id);
343+
}
344+
#endif
345+
312346
#ifdef __cplusplus
313347
}
314348
#endif

Include/internal/pycore_object.h

+14
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,20 @@ extern PyStatus _PyObject_InitState(PyInterpreterState *interp);
293293
extern void _PyObject_FiniState(PyInterpreterState *interp);
294294
extern bool _PyRefchain_IsTraced(PyInterpreterState *interp, PyObject *obj);
295295

296+
// Macros used for per-thread reference counting in the free threading build.
297+
// They resolve to normal Py_INCREF/DECREF calls in the default build.
298+
//
299+
// The macros are used for only a few references that would otherwise cause
300+
// scaling bottlenecks in the free threading build:
301+
// - The reference from an object to `ob_type`.
302+
// - The reference from a function to `func_code`.
303+
// - The reference from a function to `func_globals` and `func_builtins`.
304+
//
305+
// It's safe, but not performant or necessary, to use these macros for other
306+
// references to code, type, or dict objects. It's also safe to mix their
307+
// usage with normal Py_INCREF/DECREF calls.
308+
//
309+
// See also Include/internal/pycore_dict.h for _Py_INCREF_DICT/_Py_DECREF_DICT.
296310
#ifndef Py_GIL_DISABLED
297311
# define _Py_INCREF_TYPE Py_INCREF
298312
# define _Py_DECREF_TYPE Py_DECREF

Include/internal/pycore_uniqueid.h

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ struct _Py_unique_id_pool {
4848
// Assigns the next id from the pool of ids.
4949
extern Py_ssize_t _PyObject_AssignUniqueId(PyObject *obj);
5050

51+
// Releases the allocated id back to the pool.
52+
extern void _PyObject_ReleaseUniqueId(Py_ssize_t unique_id);
53+
5154
// Releases the allocated id back to the pool.
5255
extern void _PyObject_DisablePerThreadRefcounting(PyObject *obj);
5356

Objects/dictobject.c

+18
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,24 @@ _PyDict_MaybeUntrack(PyObject *op)
16361636
_PyObject_GC_UNTRACK(op);
16371637
}
16381638

1639+
void
1640+
_PyDict_EnablePerThreadRefcounting(PyObject *op)
1641+
{
1642+
assert(PyDict_Check(op));
1643+
#ifdef Py_GIL_DISABLED
1644+
Py_ssize_t id = _PyObject_AssignUniqueId(op);
1645+
if ((uint64_t)id >= (uint64_t)DICT_UNIQUE_ID_MAX) {
1646+
_PyObject_ReleaseUniqueId(id);
1647+
return;
1648+
}
1649+
1650+
PyDictObject *mp = (PyDictObject *)op;
1651+
assert((mp->_ma_watcher_tag >> DICT_UNIQUE_ID_SHIFT) == 0);
1652+
// Plus 1 so that _ma_watcher_tag=0 represents an unassigned id
1653+
mp->_ma_watcher_tag += ((uint64_t)id + 1) << DICT_UNIQUE_ID_SHIFT;
1654+
#endif
1655+
}
1656+
16391657
static inline int
16401658
is_unusable_slot(Py_ssize_t ix)
16411659
{

Objects/funcobject.c

+32-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "Python.h"
55
#include "pycore_ceval.h" // _PyEval_BuiltinsFromGlobals()
6+
#include "pycore_dict.h" // _Py_INCREF_DICT()
67
#include "pycore_long.h" // _PyLong_GetOne()
78
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
89
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
@@ -112,8 +113,15 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
112113
Py_XDECREF(module);
113114
return NULL;
114115
}
115-
op->func_globals = Py_NewRef(constr->fc_globals);
116-
op->func_builtins = Py_NewRef(constr->fc_builtins);
116+
_Py_INCREF_DICT(constr->fc_globals);
117+
op->func_globals = constr->fc_globals;
118+
if (PyDict_Check(constr->fc_builtins)) {
119+
_Py_INCREF_DICT(constr->fc_builtins);
120+
}
121+
else {
122+
Py_INCREF(constr->fc_builtins);
123+
}
124+
op->func_builtins = constr->fc_builtins;
117125
op->func_name = Py_NewRef(constr->fc_name);
118126
op->func_qualname = Py_NewRef(constr->fc_qualname);
119127
_Py_INCREF_CODE((PyCodeObject *)constr->fc_code);
@@ -143,7 +151,7 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
143151
{
144152
assert(globals != NULL);
145153
assert(PyDict_Check(globals));
146-
Py_INCREF(globals);
154+
_Py_INCREF_DICT(globals);
147155

148156
PyThreadState *tstate = _PyThreadState_GET();
149157

@@ -184,7 +192,12 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
184192
if (builtins == NULL) {
185193
goto error;
186194
}
187-
Py_INCREF(builtins);
195+
if (PyDict_Check(builtins)) {
196+
_Py_INCREF_DICT(builtins);
197+
}
198+
else {
199+
Py_INCREF(builtins);
200+
}
188201

189202
PyFunctionObject *op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
190203
if (op == NULL) {
@@ -1057,8 +1070,21 @@ func_clear(PyObject *self)
10571070
{
10581071
PyFunctionObject *op = _PyFunction_CAST(self);
10591072
func_clear_version(_PyInterpreterState_GET(), op);
1060-
Py_CLEAR(op->func_globals);
1061-
Py_CLEAR(op->func_builtins);
1073+
PyObject *globals = op->func_globals;
1074+
op->func_globals = NULL;
1075+
if (globals != NULL) {
1076+
_Py_DECREF_DICT(globals);
1077+
}
1078+
PyObject *builtins = op->func_builtins;
1079+
op->func_builtins = NULL;
1080+
if (builtins != NULL) {
1081+
if (PyDict_Check(builtins)) {
1082+
_Py_DECREF_DICT(builtins);
1083+
}
1084+
else {
1085+
Py_DECREF(builtins);
1086+
}
1087+
}
10621088
Py_CLEAR(op->func_module);
10631089
Py_CLEAR(op->func_defaults);
10641090
Py_CLEAR(op->func_kwdefaults);

Objects/moduleobject.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "Python.h"
55
#include "pycore_call.h" // _PyObject_CallNoArgs()
6+
#include "pycore_dict.h" // _PyDict_EnablePerThreadRefcounting()
67
#include "pycore_fileutils.h" // _Py_wgetcwd
78
#include "pycore_interp.h" // PyInterpreterState.importlib
89
#include "pycore_long.h" // _PyLong_GetOne()
@@ -105,7 +106,7 @@ new_module_notrack(PyTypeObject *mt)
105106
static void
106107
track_module(PyModuleObject *m)
107108
{
108-
_PyObject_SetDeferredRefcount(m->md_dict);
109+
_PyDict_EnablePerThreadRefcounting(m->md_dict);
109110
PyObject_GC_Track(m->md_dict);
110111

111112
_PyObject_SetDeferredRefcount((PyObject *)m);

Python/uniqueid.c

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "Python.h"
22

3+
#include "pycore_dict.h" // _PyDict_UniqueId()
34
#include "pycore_lock.h" // PyMutex_LockFlags()
45
#include "pycore_pystate.h" // _PyThreadState_GET()
56
#include "pycore_object.h" // _Py_IncRefTotal
@@ -98,8 +99,8 @@ _PyObject_AssignUniqueId(PyObject *obj)
9899
return unique_id;
99100
}
100101

101-
static void
102-
release_unique_id(Py_ssize_t unique_id)
102+
void
103+
_PyObject_ReleaseUniqueId(Py_ssize_t unique_id)
103104
{
104105
PyInterpreterState *interp = _PyInterpreterState_GET();
105106
struct _Py_unique_id_pool *pool = &interp->unique_ids;
@@ -128,6 +129,11 @@ clear_unique_id(PyObject *obj)
128129
id = co->_co_unique_id;
129130
co->_co_unique_id = -1;
130131
}
132+
else if (PyDict_Check(obj)) {
133+
PyDictObject *mp = (PyDictObject *)obj;
134+
id = _PyDict_UniqueId(mp);
135+
mp->_ma_watcher_tag &= ~(UINT64_MAX << DICT_UNIQUE_ID_SHIFT);
136+
}
131137
return id;
132138
}
133139

@@ -136,7 +142,7 @@ _PyObject_DisablePerThreadRefcounting(PyObject *obj)
136142
{
137143
Py_ssize_t id = clear_unique_id(obj);
138144
if (id >= 0) {
139-
release_unique_id(id);
145+
_PyObject_ReleaseUniqueId(id);
140146
}
141147
}
142148

0 commit comments

Comments
 (0)