gh-110481: Implement biased reference counting (gh-110764)

This commit is contained in:
Sam Gross 2023-10-30 12:06:09 -04:00 • committed by GitHub
parent 05f2f0ac92
commit 6dfb8fe023
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 511 additions and 52 deletions

View file

@ -296,6 +296,124 @@ _Py_DecRef(PyObject *o)
Py_DECREF(o);
}
#ifdef Py_NOGIL
static inline int
is_shared_refcnt_dead(Py_ssize_t shared)
{
#if SIZEOF_SIZE_T == 8
return shared == (Py_ssize_t)0xDDDDDDDDDDDDDDDD;
#else
return shared == (Py_ssize_t)0xDDDDDDDD;
#endif
}
void
_Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
{
// Should we queue the object for the owning thread to merge?
int should_queue;
Py_ssize_t new_shared;
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&o->ob_ref_shared);
do {
should_queue = (shared == 0 || shared == _Py_REF_MAYBE_WEAKREF);
if (should_queue) {
// If the object had refcount zero, not queued, and not merged,
// then we enqueue the object to be merged by the owning thread.
// In this case, we don't subtract one from the reference count
// because the queue holds a reference.
new_shared = _Py_REF_QUEUED;
}
else {
// Otherwise, subtract one from the reference count. This might
// be negative!
new_shared = shared - (1 << _Py_REF_SHARED_SHIFT);
}
#ifdef Py_REF_DEBUG
if ((_Py_REF_IS_MERGED(new_shared) && new_shared < 0) ||
is_shared_refcnt_dead(shared))
{
_Py_NegativeRefcount(filename, lineno, o);
}
#endif
} while (!_Py_atomic_compare_exchange_ssize(&o->ob_ref_shared,
&shared, new_shared));
if (should_queue) {
// TODO: the inter-thread queue is not yet implemented. For now,
// we just merge the refcount here.
Py_ssize_t refcount = _Py_ExplicitMergeRefcount(o, -1);
if (refcount == 0) {
_Py_Dealloc(o);
}
}
else if (new_shared == _Py_REF_MERGED) {
// refcount is zero AND merged
_Py_Dealloc(o);
}
}
void
_Py_DecRefShared(PyObject *o)
{
_Py_DecRefSharedDebug(o, NULL, 0);
}
void
_Py_MergeZeroLocalRefcount(PyObject *op)
{
assert(op->ob_ref_local == 0);
_Py_atomic_store_uintptr_relaxed(&op->ob_tid, 0);
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared);
if (shared == 0) {
// Fast-path: shared refcount is zero (including flags)
_Py_Dealloc(op);
return;
}
// Slow-path: atomically set the flags (low two bits) to _Py_REF_MERGED.
Py_ssize_t new_shared;
do {
new_shared = (shared & ~_Py_REF_SHARED_FLAG_MASK) | _Py_REF_MERGED;
} while (!_Py_atomic_compare_exchange_ssize(&op->ob_ref_shared,
&shared, new_shared));
if (new_shared == _Py_REF_MERGED) {
// i.e., the shared refcount is zero (only the flags are set) so we
// deallocate the object.
_Py_Dealloc(op);
}
}
Py_ssize_t
_Py_ExplicitMergeRefcount(PyObject *op, Py_ssize_t extra)
{
assert(!_Py_IsImmortal(op));
Py_ssize_t refcnt;
Py_ssize_t new_shared;
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared);
do {
refcnt = Py_ARITHMETIC_RIGHT_SHIFT(Py_ssize_t, shared, _Py_REF_SHARED_SHIFT);
if (_Py_REF_IS_MERGED(shared)) {
return refcnt;
}
refcnt += (Py_ssize_t)op->ob_ref_local;
refcnt += extra;
new_shared = _Py_REF_SHARED(refcnt, _Py_REF_MERGED);
} while (!_Py_atomic_compare_exchange_ssize(&op->ob_ref_shared,
&shared, new_shared));
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 0);
_Py_atomic_store_uintptr_relaxed(&op->ob_tid, 0);
return refcnt;
}
#endif
/**************************************/
@ -1926,10 +2044,7 @@ PyTypeObject _PyNone_Type = {
none_new, /*tp_new */
};
PyObject _Py_NoneStruct = {
{ _Py_IMMORTAL_REFCNT },
&_PyNone_Type
};
PyObject _Py_NoneStruct = _PyObject_HEAD_INIT(&_PyNone_Type);
/* NotImplemented is an object that can be used to signal that an
operation is not implemented for the given type combination. */
@ -2028,10 +2143,7 @@ PyTypeObject _PyNotImplemented_Type = {
notimplemented_new, /*tp_new */
};
PyObject _Py_NotImplementedStruct = {
{ _Py_IMMORTAL_REFCNT },
&_PyNotImplemented_Type
};
PyObject _Py_NotImplementedStruct = _PyObject_HEAD_INIT(&_PyNotImplemented_Type);
PyStatus
@ -2248,7 +2360,16 @@ new_reference(PyObject *op)
_PyTraceMalloc_NewReference(op);
}
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
#if !defined(Py_NOGIL)
op->ob_refcnt = 1;
#else
op->ob_tid = _Py_ThreadId();
op->_padding = 0;
op->ob_mutex = 0;
op->ob_gc_bits = 0;
op->ob_ref_local = 1;
op->ob_ref_shared = 0;
#endif
#ifdef Py_TRACE_REFS
_Py_AddToAllObjects(op);
#endif

View file

@ -2394,7 +2394,4 @@ static PyTypeObject _PySetDummy_Type = {
Py_TPFLAGS_DEFAULT, /*tp_flags */
};
static PyObject _dummy_struct = {
{ _Py_IMMORTAL_REFCNT },
&_PySetDummy_Type
};
static PyObject _dummy_struct = _PyObject_HEAD_INIT(&_PySetDummy_Type);

View file

@ -98,10 +98,7 @@ PyTypeObject PyEllipsis_Type = {
ellipsis_new, /* tp_new */
};
PyObject _Py_EllipsisObject = {
{ _Py_IMMORTAL_REFCNT },
&PyEllipsis_Type
};
PyObject _Py_EllipsisObject = _PyObject_HEAD_INIT(&PyEllipsis_Type);
/* Slice object implementation */

View file

@ -14967,7 +14967,7 @@ _PyUnicode_ClearInterned(PyInterpreterState *interp)
// Skip the Immortal Instance check and restore
// the two references (key and value) ignored
// by PyUnicode_InternInPlace().
s->ob_refcnt = 2;
_Py_SetMortal(s, 2);
#ifdef INTERNED_STATS
total_length += PyUnicode_GET_LENGTH(s);
#endif