Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-108362: Incremental GC implementation #108038

Merged
merged 17 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ Interpreter improvements:
New Features
============

* The cyclic garbage collector is now incremental.
This means that maximum pause times are reduced,
by an order of magnitude or more for larger heaps.

Improved Error Messages
-----------------------

Expand All @@ -101,6 +105,13 @@ Improved Error Messages
variables. See also :ref:`using-on-controlling-color`.
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)

Incremental Garbage Collection
------------------------------

* The cycle garbage collector is now incremental.
This means that maximum pause times are reduced
by an order of magnitude or more for larger heaps.

Other Language Changes
======================

Expand Down Expand Up @@ -232,6 +243,29 @@ fractions
sign handling, minimum width and grouping. (Contributed by Mark Dickinson
in :gh:`111320`.)

gc
--
* The cyclic garbage collector is now incremental, which changes the meanings
of the results of :meth:`gc.get_threshold` and :meth:`gc.get_threshold` as
well as :meth:`gc.get_count` and :meth:`gc.get_stats`.
* :meth:`gc.get_threshold` returns a three-tuple for backwards compatibility,
the first value is the threshold for young collections, as before, the second
value determines the rate at which the old collection is scanned; the
default is 10 and higher values mean that the old collection is scanned more slowly.
The third value is meangless and is always zero.
* :meth:`gc.set_threshold` ignores any items after the second.
* :meth:`gc.get_count` and :meth:`gc.get_stats`.
These functions return the same format of results as before.
The only difference is that instead of the results refering to
the young, aging and old generations, the results refer to the
young generation and the aging and collecting spaces of the old generation.

In summary, code that attempted to manipulate the behavior of the cycle GC may
not work as well as intended, but it is very unlikely to harmful.
All other code will work just fine.
Uses should avoid calling :meth:`gc.collect` unless their workload is episodic,
but that has always been the case to some extent.

glob
----

Expand Down
42 changes: 27 additions & 15 deletions Include/internal/pycore_gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ static inline int _PyObject_GC_MAY_BE_TRACKED(PyObject *obj) {

/* Bit flags for _gc_prev */
/* Bit 0 is set when tp_finalize is called */
#define _PyGC_PREV_MASK_FINALIZED (1)
#define _PyGC_PREV_MASK_FINALIZED 1
/* Bit 1 is set when the object is in generation which is GCed currently. */
#define _PyGC_PREV_MASK_COLLECTING (2)
#define _PyGC_PREV_MASK_COLLECTING 2

/* Bit 0 is set if the object belongs to old space 1 */
#define _PyGC_NEXT_MASK_OLD_SPACE_1 1

/* The (N-2) most significant bits contain the real address. */
#define _PyGC_PREV_SHIFT (2)
#define _PyGC_PREV_SHIFT 2
#define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT)

/* set for debugging information */
Expand All @@ -101,18 +105,21 @@ typedef enum {
// Lowest bit of _gc_next is used for flags only in GC.
// But it is always 0 for normal code.
static inline PyGC_Head* _PyGCHead_NEXT(PyGC_Head *gc) {
uintptr_t next = gc->_gc_next;
uintptr_t next = gc->_gc_next & _PyGC_PREV_MASK;
return (PyGC_Head*)next;
}
static inline void _PyGCHead_SET_NEXT(PyGC_Head *gc, PyGC_Head *next) {
gc->_gc_next = (uintptr_t)next;
uintptr_t unext = (uintptr_t)next;
assert((unext & ~_PyGC_PREV_MASK) == 0);
gc->_gc_next = (gc->_gc_next & ~_PyGC_PREV_MASK) | unext;
}

// Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags.
static inline PyGC_Head* _PyGCHead_PREV(PyGC_Head *gc) {
uintptr_t prev = (gc->_gc_prev & _PyGC_PREV_MASK);
return (PyGC_Head*)prev;
}

static inline void _PyGCHead_SET_PREV(PyGC_Head *gc, PyGC_Head *prev) {
uintptr_t uprev = (uintptr_t)prev;
assert((uprev & ~_PyGC_PREV_MASK) == 0);
Expand Down Expand Up @@ -198,6 +205,13 @@ struct gc_generation {
generations */
};

struct gc_collection_stats {
/* number of collected objects */
Py_ssize_t collected;
/* total number of uncollectable objects (put into gc.garbage) */
Py_ssize_t uncollectable;
};

/* Running stats per generation */
struct gc_generation_stats {
/* total number of collections */
Expand All @@ -219,8 +233,8 @@ struct _gc_runtime_state {
int enabled;
int debug;
/* linked lists of container objects */
struct gc_generation generations[NUM_GENERATIONS];
PyGC_Head *generation0;
struct gc_generation young;
struct gc_generation old[2];
/* a permanent generation which won't be collected */
struct gc_generation permanent_generation;
struct gc_generation_stats generation_stats[NUM_GENERATIONS];
Expand All @@ -233,22 +247,20 @@ struct _gc_runtime_state {
/* This is the number of objects that survived the last full
collection. It approximates the number of long lived objects
tracked by the GC.

(by "full collection", we mean a collection of the oldest
generation). */
Py_ssize_t long_lived_total;
/* This is the number of objects that survived all "non-full"
collections, and are awaiting to undergo a full collection for
the first time. */
Py_ssize_t long_lived_pending;

Py_ssize_t work_to_do;
/* Which of the old spaces is the visited space */
int visited_space;
};


extern void _PyGC_InitState(struct _gc_runtime_state *);

extern Py_ssize_t _PyGC_Collect(PyThreadState *tstate, int generation,
_PyGC_Reason reason);
extern Py_ssize_t _PyGC_CollectNoFail(PyThreadState *tstate);
extern Py_ssize_t _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason);
extern void _PyGC_CollectNoFail(PyThreadState *tstate);

/* Freeze objects tracked by the GC and ignore them in future collections. */
extern void _PyGC_Freeze(PyInterpreterState *interp);
Expand Down
17 changes: 3 additions & 14 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,7 @@ static inline void _Py_RefcntAdd(PyObject* op, Py_ssize_t n)
}
#define _Py_RefcntAdd(op, n) _Py_RefcntAdd(_PyObject_CAST(op), n)

static inline void _Py_SetImmortal(PyObject *op)
{
if (op) {
#ifdef Py_GIL_DISABLED
op->ob_tid = _Py_UNOWNED_TID;
op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL;
op->ob_ref_shared = 0;
#else
op->ob_refcnt = _Py_IMMORTAL_REFCNT;
#endif
}
}
#define _Py_SetImmortal(op) _Py_SetImmortal(_PyObject_CAST(op))
extern void _Py_SetImmortal(PyObject *op);

// Makes an immortal object mortal again with the specified refcnt. Should only
// be used during runtime finalization.
Expand Down Expand Up @@ -325,11 +313,12 @@ static inline void _PyObject_GC_TRACK(
filename, lineno, __func__);

PyInterpreterState *interp = _PyInterpreterState_GET();
PyGC_Head *generation0 = interp->gc.generation0;
PyGC_Head *generation0 = &interp->gc.young.head;
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
_PyGCHead_SET_NEXT(last, gc);
_PyGCHead_SET_PREV(gc, last);
_PyGCHead_SET_NEXT(gc, generation0);
assert((gc->_gc_next & _PyGC_NEXT_MASK_OLD_SPACE_1) == 0);
generation0->_gc_prev = (uintptr_t)gc;
#endif
}
Expand Down
8 changes: 4 additions & 4 deletions Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,12 @@ extern PyTypeObject _PyExc_MemoryError;
}, \
.gc = { \
.enabled = 1, \
.generations = { \
/* .head is set in _PyGC_InitState(). */ \
{ .threshold = 700, }, \
{ .threshold = 10, }, \
.young = { .threshold = 2000, }, \
.old = { \
{ .threshold = 10, }, \
{ .threshold = 0, }, \
}, \
.work_to_do = -5000, \
}, \
.object_state = _py_object_state_INIT(INTERP), \
.dtoa = _dtoa_state_INIT(&(INTERP)), \
Expand Down
22 changes: 2 additions & 20 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,19 +383,11 @@ def test_collect_generations(self):
# each call to collect(N)
x = []
gc.collect(0)
# x is now in gen 1
# x is now in the old gen
a, b, c = gc.get_count()
gc.collect(1)
# x is now in gen 2
d, e, f = gc.get_count()
gc.collect(2)
# x is now in gen 3
g, h, i = gc.get_count()
# We don't check a, d, g since their exact values depends on
# We don't check a since its exact values depends on
# internal implementation details of the interpreter.
self.assertEqual((b, c), (1, 0))
self.assertEqual((e, f), (0, 1))
self.assertEqual((h, i), (0, 0))

def test_trashcan(self):
class Ouch:
Expand Down Expand Up @@ -846,16 +838,6 @@ def test_get_objects_generations(self):
self.assertFalse(
any(l is element for element in gc.get_objects(generation=2))
)
gc.collect(generation=1)
self.assertFalse(
any(l is element for element in gc.get_objects(generation=0))
)
self.assertFalse(
any(l is element for element in gc.get_objects(generation=1))
)
self.assertTrue(
any(l is element for element in gc.get_objects(generation=2))
)
gc.collect(generation=2)
self.assertFalse(
any(l is element for element in gc.get_objects(generation=0))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Implements an incremental cyclic garbage collector. By collecting the old
generation in increments, there is no need for a full heap scan. This can
hugely reduce maximum pause time for programs with large heaps.

Reduces the number of generations from three to two. The old generation is
split into two spaces, "aging" and "collecting".

Collection happens in two steps:: * First, the young generation is scanned
and the survivors moved to the end of the aging space. * Then objects are
taken from the collecting space, at such a rate that all cycles are
collected eventually. Those objects are then scanned and the survivors
moved to the end of the aging space. When the collecting space becomes
empty, the two spaces are swapped.
23 changes: 9 additions & 14 deletions Modules/gcmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,12 @@ gc_set_threshold_impl(PyObject *module, int threshold0, int group_right_1,
{
GCState *gcstate = get_gc_state();

gcstate->generations[0].threshold = threshold0;
gcstate->young.threshold = threshold0;
if (group_right_1) {
gcstate->generations[1].threshold = threshold1;
gcstate->old[0].threshold = threshold1;
}
if (group_right_2) {
gcstate->generations[2].threshold = threshold2;

/* generations higher than 2 get the same threshold */
for (int i = 3; i < NUM_GENERATIONS; i++) {
gcstate->generations[i].threshold = gcstate->generations[2].threshold;
}
gcstate->old[1].threshold = threshold2;
}
Py_RETURN_NONE;
}
Expand All @@ -185,9 +180,9 @@ gc_get_threshold_impl(PyObject *module)
{
GCState *gcstate = get_gc_state();
return Py_BuildValue("(iii)",
gcstate->generations[0].threshold,
gcstate->generations[1].threshold,
gcstate->generations[2].threshold);
gcstate->young.threshold,
gcstate->old[0].threshold,
0);
}

/*[clinic input]
Expand All @@ -202,9 +197,9 @@ gc_get_count_impl(PyObject *module)
{
GCState *gcstate = get_gc_state();
return Py_BuildValue("(iii)",
gcstate->generations[0].count,
gcstate->generations[1].count,
gcstate->generations[2].count);
gcstate->young.count,
gcstate->old[gcstate->visited_space].count,
gcstate->old[gcstate->visited_space^1].count);
}

/*[clinic input]
Expand Down
15 changes: 15 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2387,6 +2387,21 @@ _Py_NewReferenceNoTotal(PyObject *op)
new_reference(op);
}

void
_Py_SetImmortal(PyObject *op)
{
if (PyObject_IS_GC(op) && _PyObject_GC_IS_TRACKED(op)) {
_PyObject_GC_UNTRACK(op);
}
#ifdef Py_GIL_DISABLED
op->ob_tid = _Py_UNOWNED_TID;
op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL;
op->ob_ref_shared = 0;
#else
op->ob_refcnt = _Py_IMMORTAL_REFCNT;
#endif
}

void
_Py_ResurrectReference(PyObject *op)
{
Expand Down
5 changes: 4 additions & 1 deletion Objects/structseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,9 @@ _PyStructSequence_InitBuiltinWithFlags(PyInterpreterState *interp,
PyStructSequence_Desc *desc,
unsigned long tp_flags)
{
if (Py_TYPE(type) == NULL) {
Py_SET_TYPE(type, &PyType_Type);
}
Py_ssize_t n_unnamed_members;
Py_ssize_t n_members = count_members(desc, &n_unnamed_members);
PyMemberDef *members = NULL;
Expand All @@ -618,7 +621,7 @@ _PyStructSequence_InitBuiltinWithFlags(PyInterpreterState *interp,
}
initialize_static_fields(type, desc, members, tp_flags);

_Py_SetImmortal(type);
_Py_SetImmortal((PyObject *)type);
}
#ifndef NDEBUG
else {
Expand Down
Loading
Loading