From 266bb9d919a4cb9cfa619d329b33f6efba139809 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Mon, 20 Apr 2026 00:46:17 +0500 Subject: [PATCH] WIP: another way --- Include/internal/pycore_gc.h | 10 +- Include/internal/pycore_interp_structs.h | 2 +- Lib/test/test_gc.py | 59 +- Modules/gcmodule.c | 6 +- Python/gc.c | 708 +++++------------------ 5 files changed, 211 insertions(+), 574 deletions(-) diff --git a/Include/internal/pycore_gc.h b/Include/internal/pycore_gc.h index fd284d0e4ecc2f..446c30dcd290c8 100644 --- a/Include/internal/pycore_gc.h +++ b/Include/internal/pycore_gc.h @@ -131,7 +131,6 @@ static inline void _PyObject_GC_SET_SHARED(PyObject *op) { * When object are moved from the pending space, old[gcstate->visited_space^1] * into the increment, the old space bit is flipped. */ -#define _PyGC_NEXT_MASK_OLD_SPACE_1 1 #define _PyGC_PREV_SHIFT 2 #define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT) @@ -159,13 +158,11 @@ 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 & _PyGC_PREV_MASK; + uintptr_t next = gc->_gc_next; return (PyGC_Head*)next; } static inline void _PyGCHead_SET_NEXT(PyGC_Head *gc, PyGC_Head *next) { - uintptr_t unext = (uintptr_t)next; - assert((unext & ~_PyGC_PREV_MASK) == 0); - gc->_gc_next = (gc->_gc_next & ~_PyGC_PREV_MASK) | unext; + gc->_gc_next = (uintptr_t)next; } // Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags. @@ -249,8 +246,7 @@ static inline void _PyObject_GC_TRACK( PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev); _PyGCHead_SET_NEXT(last, gc); _PyGCHead_SET_PREV(gc, last); - uintptr_t not_visited = 1 ^ gcstate->visited_space; - gc->_gc_next = ((uintptr_t)generation0) | not_visited; + _PyGCHead_SET_NEXT(gc, generation0); generation0->_gc_prev = (uintptr_t)gc; gcstate->young.count++; /* number of tracked GC objects */ gcstate->heap_size++; diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index c3d9533e90fdec..cdcc053c3c4458 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -233,7 +233,6 @@ struct _gc_runtime_state { int visited_space; int phase; -#ifdef Py_GIL_DISABLED /* This is the number of objects that survived the last full collection. It approximates the number of long lived objects tracked by the GC. @@ -246,6 +245,7 @@ struct _gc_runtime_state { the first time. */ Py_ssize_t long_lived_pending; +#ifdef Py_GIL_DISABLED /* True if gc.freeze() has been used. */ int freeze_active; diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 3879f116731a22..3938379f6cfd28 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -7,7 +7,7 @@ Py_GIL_DISABLED) from test.support.import_helper import import_module from test.support.os_helper import temp_dir, TESTFN, unlink -from test.support.script_helper import assert_python_ok, make_script, run_test_script +from test.support.script_helper import assert_python_ok, make_script from test.support import threading_helper, gc_threshold import gc @@ -399,11 +399,19 @@ def test_collect_generations(self): # each call to collect(N) x = [] gc.collect(0) - # x is now in the old gen + # x is now in gen 1 a, b, c = gc.get_count() - # We don't check a since its exact values depends on + 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 # 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: @@ -870,10 +878,42 @@ def test_get_objects_generations(self): self.assertTrue( any(l is element for element in gc.get_objects(generation=0)) ) - gc.collect() + self.assertFalse( + any(l is element for element in gc.get_objects(generation=1)) + ) + self.assertFalse( + any(l is element for element in gc.get_objects(generation=2)) + ) + gc.collect(generation=0) + self.assertFalse( + any(l is element for element in gc.get_objects(generation=0)) + ) + self.assertTrue( + any(l is element for element in gc.get_objects(generation=1)) + ) + 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)) + ) + 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)) + ) del l gc.collect() @@ -1181,17 +1221,6 @@ def test_tuple_untrack_counts(self): self.assertTrue(new_count - count > (n // 2)) -class IncrementalGCTests(unittest.TestCase): - @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") - @requires_gil_enabled("Free threading does not support incremental GC") - def test_incremental_gc_handles_fast_cycle_creation(self): - # Run this test in a fresh process. The number of alive objects (which can - # be from unit tests run before this one) can influence how quickly cyclic - # garbage is found. - script = support.findfile("_test_gc_fast_cycles.py") - run_test_script(script) - - class GCCallbackTests(unittest.TestCase): def setUp(self): # Save gc state and disable it. diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index a09c4856f1166d..4fa598ca4f0cd3 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -183,7 +183,7 @@ gc_get_threshold_impl(PyObject *module) return Py_BuildValue("(iii)", gcstate->young.threshold, gcstate->old[0].threshold, - 0); + gcstate->old[1].threshold); } /*[clinic input] @@ -209,8 +209,8 @@ gc_get_count_impl(PyObject *module) return Py_BuildValue("(iii)", gcstate->young.count, - gcstate->old[gcstate->visited_space].count, - gcstate->old[gcstate->visited_space^1].count); + gcstate->old[0].count, + gcstate->old[1].count); } /*[clinic input] diff --git a/Python/gc.c b/Python/gc.c index c134dc57e28f6d..2f66ac6ee5901b 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -24,9 +24,6 @@ typedef struct _gc_runtime_state GCState; # define GC_DEBUG #endif -// Define this when debugging the GC -// #define GC_EXTRA_DEBUG - #define GC_NEXT _PyGCHead_NEXT #define GC_PREV _PyGCHead_PREV @@ -50,7 +47,7 @@ typedef struct _gc_runtime_state GCState; // move_legacy_finalizers() removes this flag instead. // Between them, unreachable list is not normal list and we can not use // most gc_list_* functions for it. -#define NEXT_MASK_UNREACHABLE 2 +#define NEXT_MASK_UNREACHABLE (1) #define AS_GC(op) _Py_AS_GC(op) #define FROM_GC(gc) _Py_FROM_GC(gc) @@ -100,44 +97,33 @@ gc_decref(PyGC_Head *g) g->_gc_prev -= 1 << _PyGC_PREV_SHIFT; } -static inline int -gc_old_space(PyGC_Head *g) -{ - return g->_gc_next & _PyGC_NEXT_MASK_OLD_SPACE_1; -} - -static inline int -other_space(int space) -{ - assert(space == 0 || space == 1); - return space ^ _PyGC_NEXT_MASK_OLD_SPACE_1; -} - -static inline void -gc_flip_old_space(PyGC_Head *g) -{ - g->_gc_next ^= _PyGC_NEXT_MASK_OLD_SPACE_1; -} - -static inline void -gc_set_old_space(PyGC_Head *g, int space) -{ - assert(space == 0 || space == _PyGC_NEXT_MASK_OLD_SPACE_1); - g->_gc_next &= ~_PyGC_NEXT_MASK_OLD_SPACE_1; - g->_gc_next |= space; -} +// #define GEN_HEAD(gcstate, n) (&(gcstate)->generations[n].head) static PyGC_Head * GEN_HEAD(GCState *gcstate, int n) { - assert((gcstate->visited_space & (~1)) == 0); switch(n) { case 0: return &gcstate->young.head; case 1: - return &gcstate->old[gcstate->visited_space].head; + return &gcstate->old[0].head; + case 2: + return &gcstate->old[1].head; + default: + Py_UNREACHABLE(); + } +} + +static struct gc_generation * +GEN(GCState *gcstate, int n) +{ + switch(n) { + case 0: + return &gcstate->young; + case 1: + return &gcstate->old[0]; case 2: - return &gcstate->old[gcstate->visited_space^1].head; + return &gcstate->old[1]; default: Py_UNREACHABLE(); } @@ -264,7 +250,6 @@ gc_list_is_empty(PyGC_Head *list) static inline void gc_list_append(PyGC_Head *node, PyGC_Head *list) { - assert((list->_gc_prev & ~_PyGC_PREV_MASK) == 0); PyGC_Head *last = (PyGC_Head *)list->_gc_prev; // last <-> node @@ -322,8 +307,6 @@ gc_list_merge(PyGC_Head *from, PyGC_Head *to) PyGC_Head *from_tail = GC_PREV(from); assert(from_head != from); assert(from_tail != from); - assert(gc_list_is_empty(to) || - gc_old_space(to_tail) == gc_old_space(from_tail)); _PyGCHead_SET_NEXT(to_tail, from_head); _PyGCHead_SET_PREV(from_head, to_tail); @@ -392,8 +375,8 @@ enum flagstates {collecting_clear_unreachable_clear, static void validate_list(PyGC_Head *head, enum flagstates flags) { - assert((head->_gc_prev & ~_PyGC_PREV_MASK) == 0); - assert((head->_gc_next & ~_PyGC_PREV_MASK) == 0); + assert((head->_gc_prev & PREV_MASK_COLLECTING) == 0); + assert((head->_gc_next & NEXT_MASK_UNREACHABLE) == 0); uintptr_t prev_value = 0, next_value = 0; switch (flags) { case collecting_clear_unreachable_clear: @@ -415,7 +398,7 @@ validate_list(PyGC_Head *head, enum flagstates flags) PyGC_Head *gc = GC_NEXT(head); while (gc != head) { PyGC_Head *trueprev = GC_PREV(gc); - PyGC_Head *truenext = GC_NEXT(gc); + PyGC_Head *truenext = (PyGC_Head *)(gc->_gc_next & ~NEXT_MASK_UNREACHABLE); assert(truenext != NULL); assert(trueprev == prev); assert((gc->_gc_prev & PREV_MASK_COLLECTING) == prev_value); @@ -425,58 +408,10 @@ validate_list(PyGC_Head *head, enum flagstates flags) } assert(prev == GC_PREV(head)); } - #else #define validate_list(x, y) do{}while(0) #endif -#ifdef GC_EXTRA_DEBUG - - -static void -gc_list_validate_space(PyGC_Head *head, int space) { - PyGC_Head *gc = GC_NEXT(head); - while (gc != head) { - assert(gc_old_space(gc) == space); - gc = GC_NEXT(gc); - } -} - -static void -validate_spaces(GCState *gcstate) -{ - int visited = gcstate->visited_space; - int not_visited = other_space(visited); - gc_list_validate_space(&gcstate->young.head, not_visited); - for (int space = 0; space < 2; space++) { - gc_list_validate_space(&gcstate->old[space].head, space); - } - gc_list_validate_space(&gcstate->permanent_generation.head, visited); -} - -static void -validate_consistent_old_space(PyGC_Head *head) -{ - PyGC_Head *gc = GC_NEXT(head); - if (gc == head) { - return; - } - int old_space = gc_old_space(gc); - while (gc != head) { - PyGC_Head *truenext = GC_NEXT(gc); - assert(truenext != NULL); - assert(gc_old_space(gc) == old_space); - gc = truenext; - } -} - - -#else -#define validate_spaces(g) do{}while(0) -#define validate_consistent_old_space(l) do{}while(0) -#define gc_list_validate_space(l, s) do{}while(0) -#endif - /*** end of list stuff ***/ @@ -570,8 +505,7 @@ _PyGC_VisitFrameStack(_PyInterpreterFrame *frame, visitproc visit, void *arg) } /* Subtract internal references from gc_refs. After this, gc_refs is >= 0 - * for all objects in containers, and is GC_REACHABLE for all tracked gc - * objects not in containers. The ones with gc_refs > 0 are directly + * for all objects in containers. The ones with gc_refs > 0 are directly * reachable from outside containers, and so can't be collected. */ static void @@ -622,13 +556,12 @@ visit_reachable(PyObject *op, void *arg) // Manually unlink gc from unreachable list because the list functions // don't work right in the presence of NEXT_MASK_UNREACHABLE flags. PyGC_Head *prev = GC_PREV(gc); - PyGC_Head *next = GC_NEXT(gc); + PyGC_Head *next = (PyGC_Head*)(gc->_gc_next & ~NEXT_MASK_UNREACHABLE); _PyObject_ASSERT(FROM_GC(prev), prev->_gc_next & NEXT_MASK_UNREACHABLE); _PyObject_ASSERT(FROM_GC(next), next->_gc_next & NEXT_MASK_UNREACHABLE); - prev->_gc_next = gc->_gc_next; // copy flag bits - gc->_gc_next &= ~NEXT_MASK_UNREACHABLE; + prev->_gc_next = gc->_gc_next; // copy NEXT_MASK_UNREACHABLE _PyGCHead_SET_PREV(next, prev); gc_list_append(gc, reachable); @@ -680,9 +613,6 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable) * or to the right have been scanned yet. */ - validate_consistent_old_space(young); - /* Record which old space we are in, and set NEXT_MASK_UNREACHABLE bit for convenience */ - uintptr_t flags = NEXT_MASK_UNREACHABLE | (gc->_gc_next & _PyGC_NEXT_MASK_OLD_SPACE_1); while (gc != young) { if (gc_get_refs(gc)) { /* gc is definitely reachable from outside the @@ -728,18 +658,17 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable) // But this may pollute the unreachable list head's 'next' pointer // too. That's semantically senseless but expedient here - the // damage is repaired when this function ends. - last->_gc_next = flags | (uintptr_t)gc; + last->_gc_next = (NEXT_MASK_UNREACHABLE | (uintptr_t)gc); _PyGCHead_SET_PREV(gc, last); - gc->_gc_next = flags | (uintptr_t)unreachable; + gc->_gc_next = (NEXT_MASK_UNREACHABLE | (uintptr_t)unreachable); unreachable->_gc_prev = (uintptr_t)gc; } - gc = _PyGCHead_NEXT(prev); + gc = (PyGC_Head*)prev->_gc_next; } // young->_gc_prev must be last element remained in the list. young->_gc_prev = (uintptr_t)prev; - young->_gc_next &= _PyGC_PREV_MASK; // don't let the pollution of the list head's next pointer leak - unreachable->_gc_next &= _PyGC_PREV_MASK; + unreachable->_gc_next &= ~NEXT_MASK_UNREACHABLE; } /* In theory, all tuples should be younger than the @@ -795,8 +724,8 @@ move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers) PyObject *op = FROM_GC(gc); _PyObject_ASSERT(op, gc->_gc_next & NEXT_MASK_UNREACHABLE); - next = GC_NEXT(gc); gc->_gc_next &= ~NEXT_MASK_UNREACHABLE; + next = (PyGC_Head*)gc->_gc_next; if (has_legacy_finalizer(op)) { gc_clear_collecting(gc); @@ -819,8 +748,8 @@ clear_unreachable_mask(PyGC_Head *unreachable) PyGC_Head *gc, *next; for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) { _PyObject_ASSERT((PyObject*)FROM_GC(gc), gc->_gc_next & NEXT_MASK_UNREACHABLE); - next = GC_NEXT(gc); gc->_gc_next &= ~NEXT_MASK_UNREACHABLE; + next = (PyGC_Head*)gc->_gc_next; } validate_list(unreachable, collecting_set_unreachable_clear); } @@ -1001,7 +930,6 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old, bool allow_callbacks) /* Invoke the callbacks we decided to honor. It's safe to invoke them * because they can't reference unreachable objects. */ - int visited_space = get_gc_state()->visited_space; while (! gc_list_is_empty(&wrcb_to_call)) { PyObject *temp; PyObject *callback; @@ -1037,7 +965,6 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old, bool allow_callbacks) Py_DECREF(op); if (wrcb_to_call._gc_next == (uintptr_t)gc) { /* object is still alive -- move it */ - gc_set_old_space(gc, visited_space); gc_list_move(gc, old); } else { @@ -1282,36 +1209,6 @@ gc_collect_region(PyThreadState *tstate, PyGC_Head *to, struct gc_collection_stats *stats); -static inline Py_ssize_t -gc_list_set_space(PyGC_Head *list, int space) -{ - Py_ssize_t size = 0; - PyGC_Head *gc; - for (gc = GC_NEXT(list); gc != list; gc = GC_NEXT(gc)) { - gc_set_old_space(gc, space); - size++; - } - return size; -} - -/* Making progress in the incremental collector - * In order to eventually collect all cycles - * the incremental collector must progress through the old - * space faster than objects are added to the old space. - * - * Each young or incremental collection adds a number of - * objects, S (for survivors) to the old space, and - * incremental collectors scan I objects from the old space. - * I > S must be true. We also want I > S * N to be where - * N > 1. Higher values of N mean that the old space is - * scanned more rapidly. - * The default incremental threshold of 10 translates to - * N == 1.4 (1 + 4/threshold) - */ - -/* Divide by 10, so that the default incremental threshold of 10 - * scans objects at 1% of the heap size */ -#define SCAN_RATE_DIVISOR 10 static void add_stats(GCState *gcstate, int gen, struct gc_collection_stats *stats) @@ -1321,386 +1218,117 @@ add_stats(GCState *gcstate, int gen, struct gc_collection_stats *stats) gcstate->generation_stats[gen].collections += 1; } -static void -gc_collect_young(PyThreadState *tstate, - struct gc_collection_stats *stats) -{ - GCState *gcstate = &tstate->interp->gc; - validate_spaces(gcstate); - PyGC_Head *young = &gcstate->young.head; - PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head; - untrack_tuples(young); - GC_STAT_ADD(0, collections, 1); -#ifdef Py_STATS - { - Py_ssize_t count = 0; - PyGC_Head *gc; - for (gc = GC_NEXT(young); gc != young; gc = GC_NEXT(gc)) { - count++; - } - } -#endif - - PyGC_Head survivors; - gc_list_init(&survivors); - gc_list_set_space(young, gcstate->visited_space); - gc_collect_region(tstate, young, &survivors, stats); - gc_list_merge(&survivors, visited); - validate_spaces(gcstate); - gcstate->young.count = 0; - gcstate->old[gcstate->visited_space].count++; - add_stats(gcstate, 0, stats); - validate_spaces(gcstate); -} - -#ifndef NDEBUG -static inline int -IS_IN_VISITED(PyGC_Head *gc, int visited_space) -{ - assert(visited_space == 0 || other_space(visited_space) == 0); - return gc_old_space(gc) == visited_space; -} -#endif - -struct container_and_flag { - PyGC_Head *container; - int visited_space; - intptr_t size; -}; - -/* A traversal callback for adding to container) */ +/* Find the oldest generation (highest numbered) where the count + * exceeds the threshold. Objects in the that generation and + * generations younger than it will be collected. */ static int -visit_add_to_container(PyObject *op, void *arg) -{ - OBJECT_STAT_INC(object_visits); - struct container_and_flag *cf = (struct container_and_flag *)arg; - int visited = cf->visited_space; - assert(visited == get_gc_state()->visited_space); - if (!_Py_IsImmortal(op) && _PyObject_IS_GC(op)) { - PyGC_Head *gc = AS_GC(op); - if (_PyObject_GC_IS_TRACKED(op) && - gc_old_space(gc) != visited) { - gc_flip_old_space(gc); - gc_list_move(gc, cf->container); - cf->size++; +gc_select_generation(GCState *gcstate) +{ + for (int i = NUM_GENERATIONS-1; i >= 0; i--) { + if (GEN(gcstate, i)->count > GEN(gcstate, i)->threshold) { + /* Avoid quadratic performance degradation in number + of tracked objects (see also issue #4074): + + To limit the cost of garbage collection, there are two strategies; + - make each collection faster, e.g. by scanning fewer objects + - do less collections + This heuristic is about the latter strategy. + + In addition to the various configurable thresholds, we only trigger a + full collection if the ratio + + long_lived_pending / long_lived_total + + is above a given value (hardwired to 25%). + + The reason is that, while "non-full" collections (i.e., collections of + the young and middle generations) will always examine roughly the same + number of objects -- determined by the aforementioned thresholds --, + the cost of a full collection is proportional to the total number of + long-lived objects, which is virtually unbounded. + + Indeed, it has been remarked that doing a full collection every + of object creations entails a dramatic performance + degradation in workloads which consist in creating and storing lots of + long-lived objects (e.g. building a large list of GC-tracked objects would + show quadratic performance, instead of linear as expected: see issue #4074). + + Using the above ratio, instead, yields amortized linear performance in + the total number of objects (the effect of which can be summarized + thusly: "each full garbage collection is more and more costly as the + number of objects grows, but we do fewer and fewer of them"). + + This heuristic was suggested by Martin von Löwis on python-dev in + June 2008. His original analysis and proposal can be found at: + http://mail.python.org/pipermail/python-dev/2008-June/080579.html + */ + if (i == NUM_GENERATIONS - 1 + && gcstate->long_lived_pending < gcstate->long_lived_total / 4) + { + continue; + } + return i; } } - return 0; + return -1; } -static intptr_t -expand_region_transitively_reachable(PyGC_Head *container, PyGC_Head *gc, GCState *gcstate) +static Py_ssize_t +gc_collect_generational(PyThreadState *tstate, int generation, struct gc_collection_stats *stats) { - struct container_and_flag arg = { - .container = container, - .visited_space = gcstate->visited_space, - .size = 0 - }; - assert(GC_NEXT(gc) == container); - while (gc != container) { - /* Survivors will be moved to visited space, so they should - * have been marked as visited */ - assert(IS_IN_VISITED(gc, gcstate->visited_space)); - PyObject *op = FROM_GC(gc); - assert(_PyObject_GC_IS_TRACKED(op)); - if (_Py_IsImmortal(op)) { - PyGC_Head *next = GC_NEXT(gc); - gc_list_move(gc, &get_gc_state()->permanent_generation.head); - gc = next; - continue; + int i; + GCState *gcstate = &tstate->interp->gc; + PyGC_Head *young; /* the generation we are examining */ + PyGC_Head *old; /* next older generation */ + + if (generation == GENERATION_AUTO) { + // Select the oldest generation that needs collecting. We will collect + // objects from that generation and all generations younger than it. + generation = gc_select_generation(gcstate); + if (generation < 0) { + // No generation needs to be collected. + return generation; } - traverseproc traverse = Py_TYPE(op)->tp_traverse; - (void) traverse(op, - visit_add_to_container, - &arg); - gc = GC_NEXT(gc); } - return arg.size; -} -/* Do bookkeeping for a completed GC cycle */ -static void -completed_scavenge(GCState *gcstate) -{ - /* We must observe two invariants: - * 1. Members of the permanent generation must be marked visited. - * 2. We cannot touch members of the permanent generation. */ - int visited; - if (gc_list_is_empty(&gcstate->permanent_generation.head)) { - /* Permanent generation is empty so we can flip spaces bit */ - int not_visited = gcstate->visited_space; - visited = other_space(not_visited); - gcstate->visited_space = visited; - /* Make sure all objects have visited bit set correctly */ - gc_list_set_space(&gcstate->young.head, not_visited); - } - else { - /* We must move the objects from visited to pending space. */ - visited = gcstate->visited_space; - int not_visited = other_space(visited); - assert(gc_list_is_empty(&gcstate->old[not_visited].head)); - gc_list_merge(&gcstate->old[visited].head, &gcstate->old[not_visited].head); - gc_list_set_space(&gcstate->old[not_visited].head, not_visited); - } - assert(gc_list_is_empty(&gcstate->old[visited].head)); - gcstate->work_to_do = 0; - gcstate->phase = GC_PHASE_MARK; -} + assert(generation >= 0 && generation < NUM_GENERATIONS); + GC_STAT_ADD(generation, collections, 1); -static intptr_t -move_to_reachable(PyObject *op, PyGC_Head *reachable, int visited_space) -{ - if (op != NULL && !_Py_IsImmortal(op) && _PyObject_IS_GC(op)) { - PyGC_Head *gc = AS_GC(op); - if (_PyObject_GC_IS_TRACKED(op) && - gc_old_space(gc) != visited_space) { - gc_flip_old_space(gc); - gc_list_move(gc, reachable); - return 1; - } + /* update collection and allocation counters */ + if (generation+1 < NUM_GENERATIONS) { + GEN(gcstate, generation+1)->count += 1; } - return 0; -} - -static intptr_t -mark_all_reachable(PyGC_Head *reachable, PyGC_Head *visited, int visited_space) -{ - // Transitively traverse all objects from reachable, until empty - struct container_and_flag arg = { - .container = reachable, - .visited_space = visited_space, - .size = 0 - }; - while (!gc_list_is_empty(reachable)) { - PyGC_Head *gc = _PyGCHead_NEXT(reachable); - assert(gc_old_space(gc) == visited_space); - gc_list_move(gc, visited); - PyObject *op = FROM_GC(gc); - traverseproc traverse = Py_TYPE(op)->tp_traverse; - (void) traverse(op, - visit_add_to_container, - &arg); - } - gc_list_validate_space(visited, visited_space); - return arg.size; -} - -static intptr_t -mark_stacks(PyInterpreterState *interp, PyGC_Head *visited, int visited_space, bool start) -{ - PyGC_Head reachable; - gc_list_init(&reachable); - Py_ssize_t objects_marked = 0; - // Move all objects on stacks to reachable - _PyRuntimeState *runtime = &_PyRuntime; - HEAD_LOCK(runtime); - PyThreadState* ts = PyInterpreterState_ThreadHead(interp); - HEAD_UNLOCK(runtime); - while (ts) { - _PyInterpreterFrame *frame = ts->current_frame; - while (frame) { - if (frame->owner >= FRAME_OWNED_BY_INTERPRETER) { - frame = frame->previous; - continue; - } - _PyStackRef *locals = frame->localsplus; - _PyStackRef *sp = frame->stackpointer; - objects_marked += move_to_reachable(frame->f_locals, &reachable, visited_space); - PyObject *func = PyStackRef_AsPyObjectBorrow(frame->f_funcobj); - objects_marked += move_to_reachable(func, &reachable, visited_space); - while (sp > locals) { - sp--; - if (PyStackRef_IsNullOrInt(*sp)) { - continue; - } - PyObject *op = PyStackRef_AsPyObjectBorrow(*sp); - if (_Py_IsImmortal(op)) { - continue; - } - if (_PyObject_IS_GC(op)) { - PyGC_Head *gc = AS_GC(op); - if (_PyObject_GC_IS_TRACKED(op) && - gc_old_space(gc) != visited_space) { - gc_flip_old_space(gc); - objects_marked++; - gc_list_move(gc, &reachable); - } - } - } - if (!start && frame->visited) { - // If this frame has already been visited, then the lower frames - // will have already been visited and will not have changed - break; - } - frame->visited = 1; - frame = frame->previous; - } - HEAD_LOCK(runtime); - ts = PyThreadState_Next(ts); - HEAD_UNLOCK(runtime); + for (i = 0; i <= generation; i++) { + GEN(gcstate, i)->count = 0; } - objects_marked += mark_all_reachable(&reachable, visited, visited_space); - assert(gc_list_is_empty(&reachable)); - return objects_marked; -} -static intptr_t -mark_global_roots(PyInterpreterState *interp, PyGC_Head *visited, int visited_space) -{ - PyGC_Head reachable; - gc_list_init(&reachable); - Py_ssize_t objects_marked = 0; - objects_marked += move_to_reachable(interp->sysdict, &reachable, visited_space); - objects_marked += move_to_reachable(interp->builtins, &reachable, visited_space); - objects_marked += move_to_reachable(interp->dict, &reachable, visited_space); - struct types_state *types = &interp->types; - for (int i = 0; i < _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES; i++) { - objects_marked += move_to_reachable(types->builtins.initialized[i].tp_dict, &reachable, visited_space); - objects_marked += move_to_reachable(types->builtins.initialized[i].tp_subclasses, &reachable, visited_space); - } - for (int i = 0; i < _Py_MAX_MANAGED_STATIC_EXT_TYPES; i++) { - objects_marked += move_to_reachable(types->for_extensions.initialized[i].tp_dict, &reachable, visited_space); - objects_marked += move_to_reachable(types->for_extensions.initialized[i].tp_subclasses, &reachable, visited_space); + /* merge younger generations with one we are currently collecting */ + for (i = 0; i < generation; i++) { + gc_list_merge(GEN_HEAD(gcstate, i), GEN_HEAD(gcstate, generation)); } - objects_marked += mark_all_reachable(&reachable, visited, visited_space); - assert(gc_list_is_empty(&reachable)); - return objects_marked; -} -static intptr_t -mark_at_start(PyThreadState *tstate) -{ - // TO DO -- Make this incremental - GCState *gcstate = &tstate->interp->gc; - PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head; - Py_ssize_t objects_marked = mark_global_roots(tstate->interp, visited, gcstate->visited_space); - objects_marked += mark_stacks(tstate->interp, visited, gcstate->visited_space, true); - gcstate->work_to_do -= objects_marked; - gcstate->phase = GC_PHASE_COLLECT; - validate_spaces(gcstate); - return objects_marked; -} - -static intptr_t -assess_work_to_do(GCState *gcstate) -{ - /* The amount of work we want to do depends on three things. - * 1. The number of new objects created - * 2. The growth in heap size since the last collection - * 3. The heap size (up to the number of new objects, to avoid quadratic effects) - * - * For a steady state heap, the amount of work to do is three times the number - * of new objects added to the heap. This ensures that we stay ahead in the - * worst case of all new objects being garbage. - * - * This could be improved by tracking survival rates, but it is still a - * large improvement on the non-marking approach. - */ - intptr_t scale_factor = gcstate->old[0].threshold; - if (scale_factor < 2) { - scale_factor = 2; + /* handy references */ + young = GEN_HEAD(gcstate, generation); + if (generation < NUM_GENERATIONS-1) { + old = GEN_HEAD(gcstate, generation+1); } - intptr_t new_objects = gcstate->young.count; - intptr_t max_heap_fraction = new_objects*2; - intptr_t heap_fraction = gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor; - if (heap_fraction > max_heap_fraction) { - heap_fraction = max_heap_fraction; + else { + old = young; } - gcstate->young.count = 0; - return new_objects + heap_fraction; -} + validate_list(old, collecting_clear_unreachable_clear); + gc_collect_region(tstate, young, old, stats); -static void -gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats) -{ - GC_STAT_ADD(1, collections, 1); - GCState *gcstate = &tstate->interp->gc; - gcstate->work_to_do += assess_work_to_do(gcstate); - if (gcstate->work_to_do < 0) { - return; + /* Clear free list only during the collection of the highest + * generation */ + if (generation == NUM_GENERATIONS-1) { + _PyGC_ClearAllFreeLists(tstate->interp); } - untrack_tuples(&gcstate->young.head); - if (gcstate->phase == GC_PHASE_MARK) { - Py_ssize_t objects_marked = mark_at_start(tstate); - GC_STAT_ADD(1, objects_transitively_reachable, objects_marked); - gcstate->work_to_do -= objects_marked; - validate_spaces(gcstate); - return; - } - PyGC_Head *not_visited = &gcstate->old[gcstate->visited_space^1].head; - PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head; - PyGC_Head increment; - gc_list_init(&increment); - int scale_factor = gcstate->old[0].threshold; - if (scale_factor < 2) { - scale_factor = 2; - } - intptr_t objects_marked = mark_stacks(tstate->interp, visited, gcstate->visited_space, false); - GC_STAT_ADD(1, objects_transitively_reachable, objects_marked); - gcstate->work_to_do -= objects_marked; - gc_list_set_space(&gcstate->young.head, gcstate->visited_space); - gc_list_merge(&gcstate->young.head, &increment); - gc_list_validate_space(&increment, gcstate->visited_space); - Py_ssize_t increment_size = gc_list_size(&increment); - while (increment_size < gcstate->work_to_do) { - if (gc_list_is_empty(not_visited)) { - break; - } - PyGC_Head *gc = _PyGCHead_NEXT(not_visited); - gc_list_move(gc, &increment); - increment_size++; - assert(!_Py_IsImmortal(FROM_GC(gc))); - gc_set_old_space(gc, gcstate->visited_space); - increment_size += expand_region_transitively_reachable(&increment, gc, gcstate); - } - GC_STAT_ADD(1, objects_not_transitively_reachable, increment_size); - validate_list(&increment, collecting_clear_unreachable_clear); - gc_list_validate_space(&increment, gcstate->visited_space); - PyGC_Head survivors; - gc_list_init(&survivors); - gc_collect_region(tstate, &increment, &survivors, stats); - gc_list_merge(&survivors, visited); - assert(gc_list_is_empty(&increment)); - gcstate->work_to_do -= increment_size; - - add_stats(gcstate, 1, stats); - if (gc_list_is_empty(not_visited)) { - completed_scavenge(gcstate); - } - validate_spaces(gcstate); -} -static void -gc_collect_full(PyThreadState *tstate, - struct gc_collection_stats *stats) -{ - GC_STAT_ADD(2, collections, 1); - GCState *gcstate = &tstate->interp->gc; - validate_spaces(gcstate); - PyGC_Head *young = &gcstate->young.head; - PyGC_Head *pending = &gcstate->old[gcstate->visited_space^1].head; - PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head; - untrack_tuples(young); - /* merge all generations into visited */ - gc_list_merge(young, pending); - gc_list_validate_space(pending, 1-gcstate->visited_space); - gc_list_set_space(pending, gcstate->visited_space); - gcstate->young.count = 0; - gc_list_merge(pending, visited); - validate_spaces(gcstate); - - gc_collect_region(tstate, visited, visited, - stats); - validate_spaces(gcstate); - gcstate->young.count = 0; - gcstate->old[0].count = 0; - gcstate->old[1].count = 0; - completed_scavenge(gcstate); - _PyGC_ClearAllFreeLists(tstate->interp); - validate_spaces(gcstate); - add_stats(gcstate, 2, stats); + add_stats(gcstate, generation, stats); + return generation; } + /* This is the main function. Read this to understand how the * collection process works. */ static void @@ -1719,13 +1347,21 @@ gc_collect_region(PyThreadState *tstate, gc_list_init(&unreachable); deduce_unreachable(from, &unreachable); - validate_consistent_old_space(from); untrack_tuples(from); - validate_consistent_old_space(to); if (from != to) { + if (from == GEN_HEAD(gcstate, NUM_GENERATIONS-2)) { + gcstate->long_lived_pending += gc_list_size(from); + } gc_list_merge(from, to); } - validate_consistent_old_space(to); + else { + /* We only un-track dicts in full collections, to avoid quadratic + dict build-up. See issue #14775. + Note: _PyDict_MaybeUntrack was removed in 3.14, so dict + untracking during GC is no longer done. */ + gcstate->long_lived_pending = 0; + gcstate->long_lived_total = gc_list_size(from); + } /* Move reachable objects to next generation. */ /* All objects in unreachable are trash, but objects reachable from @@ -1751,7 +1387,6 @@ gc_collect_region(PyThreadState *tstate, /* Clear weakrefs and invoke callbacks as necessary. */ stats->collected += handle_weakrefs(&unreachable, to, true); - gc_list_validate_space(to, gcstate->visited_space); validate_list(to, collecting_clear_unreachable_clear); validate_list(&unreachable, collecting_set_unreachable_clear); @@ -1793,7 +1428,6 @@ gc_collect_region(PyThreadState *tstate, * this if they insist on creating this type of structure. */ handle_legacy_finalizers(tstate, gcstate, &finalizers, to); - gc_list_validate_space(to, gcstate->visited_space); validate_list(to, collecting_clear_unreachable_clear); } @@ -1915,25 +1549,20 @@ _PyGC_GetObjects(PyInterpreterState *interp, int generation) GCState *gcstate = &interp->gc; PyObject *result = PyList_New(0); - /* Generation: - * -1: Return all objects - * 0: All young objects - * 1: No objects - * 2: All old objects - */ - if (result == NULL || generation == 1) { - return result; + if (result == NULL) { + return NULL; } - if (generation <= 0) { - if (append_objects(result, &gcstate->young.head)) { - goto error; + + if (generation == -1) { + /* If generation is -1, get all objects from all generations */ + for (int i = 0; i < NUM_GENERATIONS; i++) { + if (append_objects(result, GEN_HEAD(gcstate, i))) { + goto error; + } } } - if (generation != 0) { - if (append_objects(result, &gcstate->old[0].head)) { - goto error; - } - if (append_objects(result, &gcstate->old[1].head)) { + else { + if (append_objects(result, GEN_HEAD(gcstate, generation))) { goto error; } } @@ -1948,23 +1577,10 @@ void _PyGC_Freeze(PyInterpreterState *interp) { GCState *gcstate = &interp->gc; - /* The permanent_generation must be visited */ - gc_list_set_space(&gcstate->young.head, gcstate->visited_space); - gc_list_merge(&gcstate->young.head, &gcstate->permanent_generation.head); - gcstate->young.count = 0; - PyGC_Head*old0 = &gcstate->old[0].head; - PyGC_Head*old1 = &gcstate->old[1].head; - if (gcstate->visited_space) { - gc_list_set_space(old0, 1); - } - else { - gc_list_set_space(old1, 0); + for (int i = 0; i < NUM_GENERATIONS; ++i) { + gc_list_merge(GEN_HEAD(gcstate, i), &gcstate->permanent_generation.head); + GEN(gcstate, i)->count = 0; } - gc_list_merge(old0, &gcstate->permanent_generation.head); - gcstate->old[0].count = 0; - gc_list_merge(old1, &gcstate->permanent_generation.head); - gcstate->old[1].count = 0; - validate_spaces(gcstate); } void @@ -1972,8 +1588,7 @@ _PyGC_Unfreeze(PyInterpreterState *interp) { GCState *gcstate = &interp->gc; gc_list_merge(&gcstate->permanent_generation.head, - &gcstate->old[gcstate->visited_space].head); - validate_spaces(gcstate); + GEN_HEAD(gcstate, NUM_GENERATIONS-1)); } Py_ssize_t @@ -2053,19 +1668,17 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason) PyDTrace_GC_START(generation); } PyObject *exc = _PyErr_GetRaisedException(tstate); - switch(generation) { - case 0: - gc_collect_young(tstate, &stats); - break; - case 1: - gc_collect_increment(tstate, &stats); - break; - case 2: - gc_collect_full(tstate, &stats); - break; - default: - Py_UNREACHABLE(); + if (gc_collect_generational(tstate, generation, &stats) < 0) { + if (_PyErr_Occurred(tstate)) { + if (reason == _Py_GC_REASON_SHUTDOWN) { + _PyErr_Clear(tstate); + } + else { + PyErr_FormatUnraisable("Exception ignored in garbage collection"); + } + } } + if (PyDTrace_GC_DONE_ENABLED()) { PyDTrace_GC_DONE(stats.uncollectable + stats.collected); } @@ -2081,7 +1694,6 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason) _Py_stats->object_stats.object_visits = 0; } #endif - validate_spaces(gcstate); _Py_atomic_store_int(&gcstate->collecting, 0); if (gcstate->debug & _PyGC_DEBUG_STATS) { @@ -2188,9 +1800,9 @@ _PyGC_Fini(PyInterpreterState *interp) * This bug was originally fixed when reported as gh-90228. The bug was * re-introduced in gh-94673. */ - finalize_unlink_gc_head(&gcstate->young.head); - finalize_unlink_gc_head(&gcstate->old[0].head); - finalize_unlink_gc_head(&gcstate->old[1].head); + for (int i = 0; i < NUM_GENERATIONS; i++) { + finalize_unlink_gc_head(GEN_HEAD(gcstate, i)); + } finalize_unlink_gc_head(&gcstate->permanent_generation.head); } @@ -2292,7 +1904,7 @@ void _Py_RunGC(PyThreadState *tstate) { if (tstate->interp->gc.enabled) { - _PyGC_Collect(tstate, 1, _Py_GC_REASON_HEAP); + _PyGC_Collect(tstate, GENERATION_AUTO, _Py_GC_REASON_HEAP); } }