diff --git a/Doc/library/gc.rst b/Doc/library/gc.rst index 652475886fc30f..cd30a186a8cae9 100644 --- a/Doc/library/gc.rst +++ b/Doc/library/gc.rst @@ -37,18 +37,11 @@ The :mod:`!gc` module provides the following functions: .. function:: collect(generation=2) - Perform a collection. The optional argument *generation* + With no arguments, run a full collection. The optional argument *generation* may be an integer specifying which generation to collect (from 0 to 2). A :exc:`ValueError` is raised if the generation number is invalid. The sum of collected objects and uncollectable objects is returned. - Calling ``gc.collect(0)`` will perform a GC collection on the young generation. - - Calling ``gc.collect(1)`` will perform a GC collection on the young generation - and an increment of the old generation. - - Calling ``gc.collect(2)`` or ``gc.collect()`` performs a full collection - The free lists maintained for a number of built-in types are cleared whenever a full collection or collection of the highest generation (2) is run. Not all items in some free lists may be freed due to the @@ -57,9 +50,6 @@ The :mod:`!gc` module provides the following functions: The effect of calling ``gc.collect()`` while the interpreter is already performing a collection is undefined. - .. versionchanged:: 3.14 - ``generation=1`` performs an increment of collection. - .. function:: set_debug(flags) @@ -75,20 +65,13 @@ The :mod:`!gc` module provides the following functions: .. function:: get_objects(generation=None) - Returns a list of all objects tracked by the collector, excluding the list - returned. If *generation* is not ``None``, return only the objects as follows: - - * 0: All objects in the young generation - * 1: No objects, as there is no generation 1 (as of Python 3.14) - * 2: All objects in the old generation + returned. If *generation* is not ``None``, return only the objects tracked by + the collector that are in that generation. .. versionchanged:: 3.8 New *generation* parameter. - .. versionchanged:: 3.14 - Generation 1 is removed - .. audit-event:: gc.get_objects generation gc.get_objects .. function:: get_stats() @@ -124,32 +107,26 @@ The :mod:`!gc` module provides the following functions: Set the garbage collection thresholds (the collection frequency). Setting *threshold0* to zero disables collection. - The GC classifies objects into two generations depending on whether they have - survived a collection. New objects are placed in the young generation. If an - object survives a collection it is moved into the old generation. - - In order to decide when to run, the collector keeps track of the number of object + The GC classifies objects into three generations depending on how many + collection sweeps they have survived. New objects are placed in the youngest + generation (generation ``0``). If an object survives a collection it is moved + into the next older generation. Since generation ``2`` is the oldest + generation, objects in that generation remain there after a collection. In + order to decide when to run, the collector keeps track of the number object allocations and deallocations since the last collection. When the number of allocations minus the number of deallocations exceeds *threshold0*, collection - starts. For each collection, all the objects in the young generation and some - fraction of the old generation is collected. + starts. Initially only generation ``0`` is examined. If generation ``0`` has + been examined more than *threshold1* times since generation ``1`` has been + examined, then generation ``1`` is examined as well. + With the third generation, things are a bit more complicated, + see `Collecting the oldest generation `_ for more information. In the free-threaded build, the increase in process memory usage is also checked before running the collector. If the memory usage has not increased by 10% since the last collection and the net number of object allocations has not exceeded 40 times *threshold0*, the collection is not run. - The fraction of the old generation that is collected is **inversely** proportional - to *threshold1*. The larger *threshold1* is, the slower objects in the old generation - are collected. - For the default value of 10, 1% of the old generation is scanned during each collection. - - *threshold2* is ignored. - - See `Garbage collector design `_ for more information. - - .. versionchanged:: 3.14 - *threshold2* is ignored + See `Garbage collector design `_ for more information. .. function:: get_count() diff --git a/Include/internal/pycore_debug_offsets.h b/Include/internal/pycore_debug_offsets.h index c166f963da4f66..66f14e69f33f44 100644 --- a/Include/internal/pycore_debug_offsets.h +++ b/Include/internal/pycore_debug_offsets.h @@ -222,8 +222,6 @@ typedef struct _Py_DebugOffsets { uint64_t size; uint64_t collecting; uint64_t frame; - uint64_t generation_stats_size; - uint64_t generation_stats; } gc; // Generator object offset; @@ -375,8 +373,6 @@ typedef struct _Py_DebugOffsets { .size = sizeof(struct _gc_runtime_state), \ .collecting = offsetof(struct _gc_runtime_state, collecting), \ .frame = offsetof(struct _gc_runtime_state, frame), \ - .generation_stats_size = sizeof(struct gc_stats), \ - .generation_stats = offsetof(struct _gc_runtime_state, generation_stats), \ }, \ .gen_object = { \ .size = sizeof(PyGenObject), \ diff --git a/Include/internal/pycore_gc.h b/Include/internal/pycore_gc.h index fd284d0e4ecc2f..7699ef09926a27 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. @@ -207,10 +204,6 @@ static inline void _PyGC_CLEAR_FINALIZED(PyObject *op) { extern void _Py_ScheduleGC(PyThreadState *tstate); -#ifndef Py_GIL_DISABLED -extern void _Py_TriggerGC(struct _gc_runtime_state *gcstate); -#endif - /* Tell the GC to track this object. * @@ -220,7 +213,7 @@ extern void _Py_TriggerGC(struct _gc_runtime_state *gcstate); * ob_traverse method. * * Internal note: interp->gc.generation0->_gc_prev doesn't have any bit flags - * because it's not object header. So we don't use _PyGCHead_PREV() and + * because it's not an object header. So we don't use _PyGCHead_PREV() and * _PyGCHead_SET_PREV() for it to avoid unnecessary bitwise operations. * * See also the public PyObject_GC_Track() function. @@ -244,19 +237,12 @@ static inline void _PyObject_GC_TRACK( "object is in generation which is garbage collected", filename, lineno, __func__); - struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc; - PyGC_Head *generation0 = &gcstate->young.head; + PyGC_Head *generation0 = _PyInterpreterState_GET()->gc.generation0; 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++; - if (gcstate->young.count > gcstate->young.threshold) { - _Py_TriggerGC(gcstate); - } #endif } @@ -291,11 +277,6 @@ static inline void _PyObject_GC_UNTRACK( _PyGCHead_SET_PREV(next, prev); gc->_gc_next = 0; gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED; - struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc; - if (gcstate->young.count > 0) { - gcstate->young.count--; - } - gcstate->heap_size--; #endif } diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 2bfb84da36cbc8..e2a27cadab2e86 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -177,78 +177,55 @@ 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; + // Total number of objects considered for collection and traversed: + Py_ssize_t candidates; + // Duration of the collection in seconds: + double duration; +}; + /* Running stats per generation */ struct gc_generation_stats { - PyTime_t ts_start; - PyTime_t ts_stop; - - /* heap_size on the start of the collection */ - Py_ssize_t heap_size; - - /* work_to_do on the start of the collection */ - Py_ssize_t work_to_do; - /* total number of collections */ Py_ssize_t collections; - - /* total number of visited objects */ - Py_ssize_t object_visits; - /* total number of collected objects */ Py_ssize_t collected; /* total number of uncollectable objects (put into gc.garbage) */ Py_ssize_t uncollectable; // Total number of objects considered for collection and traversed: Py_ssize_t candidates; - - Py_ssize_t objects_transitively_reachable; - Py_ssize_t objects_not_transitively_reachable; - - // Total duration of the collection in seconds: + // Duration of the collection in seconds: double duration; }; -#ifdef Py_GIL_DISABLED -#define GC_YOUNG_STATS_SIZE 1 -#define GC_OLD_STATS_SIZE 1 -#else -#define GC_YOUNG_STATS_SIZE 11 -#define GC_OLD_STATS_SIZE 3 -#endif -struct gc_young_stats_buffer { - struct gc_generation_stats items[GC_YOUNG_STATS_SIZE]; - int8_t index; -}; - -struct gc_old_stats_buffer { - struct gc_generation_stats items[GC_OLD_STATS_SIZE]; - int8_t index; -}; - enum _GCPhase { GC_PHASE_MARK = 0, GC_PHASE_COLLECT = 1 }; /* If we change this, we need to change the default value in the - signature of gc.collect and change the size of PyStats.gc_stats */ + signature of gc.collect */ #define NUM_GENERATIONS 3 -struct gc_stats { - struct gc_young_stats_buffer young; - struct gc_old_stats_buffer old[2]; -}; - struct _gc_runtime_state { /* Is automatic collection enabled? */ int enabled; int debug; /* linked lists of container objects */ +#ifndef Py_GIL_DISABLED + struct gc_generation generations[NUM_GENERATIONS]; + PyGC_Head *generation0; +#else struct gc_generation young; struct gc_generation old[2]; +#endif /* a permanent generation which won't be collected */ struct gc_generation permanent_generation; - struct gc_stats *generation_stats; + struct gc_generation_stats generation_stats[NUM_GENERATIONS]; /* true if we are currently running the collector */ int collecting; // The frame that started the current collection. It might be NULL even when @@ -259,13 +236,6 @@ struct _gc_runtime_state { /* a list of callbacks to be invoked when collection is performed */ PyObject *callbacks; - Py_ssize_t heap_size; - Py_ssize_t work_to_do; - /* Which of the old spaces is the visited space */ - 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. @@ -278,6 +248,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; @@ -293,6 +264,22 @@ struct _gc_runtime_state { #endif }; +#ifndef Py_GIL_DISABLED +#define GC_GENERATION_INIT \ + .generations = { \ + { .threshold = 2000, }, \ + { .threshold = 10, }, \ + { .threshold = 10, }, \ + }, +#else +#define GC_GENERATION_INIT \ + .young = { .threshold = 2000, }, \ + .old = { \ + { .threshold = 10, }, \ + { .threshold = 10, }, \ + }, +#endif + #include "pycore_gil.h" // struct _gil_runtime_state /**** Import ********/ diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index e8d1098c2078fc..6c48ac0dccfcb1 100644 --- a/Include/internal/pycore_runtime_init.h +++ b/Include/internal/pycore_runtime_init.h @@ -130,13 +130,7 @@ extern PyTypeObject _PyExc_MemoryError; }, \ .gc = { \ .enabled = 1, \ - .young = { .threshold = 2000, }, \ - .old = { \ - { .threshold = 10, }, \ - { .threshold = 0, }, \ - }, \ - .work_to_do = -5000, \ - .phase = GC_PHASE_MARK, \ + GC_GENERATION_INIT \ }, \ .qsbr = { \ .wr_seq = QSBR_INITIAL, \ diff --git a/InternalDocs/garbage_collector.md b/InternalDocs/garbage_collector.md index 94e6fb05b68d6f..a8c43e211c66de 100644 --- a/InternalDocs/garbage_collector.md +++ b/InternalDocs/garbage_collector.md @@ -107,7 +107,7 @@ As is explained later in the [Optimization: reusing fields to save memory](#optimization-reusing-fields-to-save-memory) section, these two extra fields are normally used to keep doubly linked lists of all the objects tracked by the garbage collector (these lists are the GC generations, more on -that in the [Optimization: incremental collection](#Optimization-incremental-collection) section), but +that in the [Optimization: generations](#Optimization-generations) section), but they are also reused to fulfill other purposes when the full doubly linked list structure is not needed as a memory optimization. @@ -203,22 +203,22 @@ unreachable: ```pycon >>> import gc ->>> +>>> >>> class Link: ... def __init__(self, next_link=None): ... self.next_link = next_link -... +... >>> link_3 = Link() >>> link_2 = Link(link_3) >>> link_1 = Link(link_2) >>> link_3.next_link = link_1 >>> A = link_1 >>> del link_1, link_2, link_3 ->>> +>>> >>> link_4 = Link() >>> link_4.next_link = link_4 >>> del link_4 ->>> +>>> >>> # Collect the unreachable Link object (and its .__dict__ dict). >>> gc.collect() 2 @@ -360,11 +360,12 @@ follows these steps in order: the reference counts fall to 0, triggering the destruction of all unreachable objects. -Optimization: incremental collection -==================================== +Optimization: generations +========================= -In order to bound the length of each garbage collection pause, the GC implementation -for the default build uses incremental collection with two generations. +In order to limit the time each garbage collection takes, the GC +implementation for the default build uses a popular optimization: +generations. Generational garbage collection takes advantage of what is known as the weak generational hypothesis: Most objects die young. @@ -372,76 +373,29 @@ This has proven to be very close to the reality of many Python programs as many temporary objects are created and destroyed very quickly. To take advantage of this fact, all container objects are segregated into -two generations: young and old. Every new object starts in the young generation. -Each garbage collection scans the entire young generation and part of the old generation. - -The time taken to scan the young generation can be controlled by controlling its -size, but the size of the old generation cannot be controlled. -In order to keep pause times down, scanning of the old generation of the heap -occurs in increments. - -To keep track of what has been scanned, the old generation contains two lists: - -* Those objects that have not yet been scanned, referred to as the `pending` list. -* Those objects that have been scanned, referred to as the `visited` list. - -To detect and collect all unreachable objects in the heap, the garbage collector -must scan the whole heap. This whole heap scan is called a full scavenge. - -Increments ----------- - -Each full scavenge is performed in a series of increments. -For each full scavenge, the combined increments will cover the whole heap. - -Each increment is made up of: - -* The young generation -* The old generation's least recently scanned objects -* All objects reachable from those objects that have not yet been scanned this full scavenge - -The surviving objects (those that are not collected) are moved to the back of the -`visited` list in the old generation. - -When a full scavenge starts, no objects in the heap are considered to have been scanned, -so all objects in the old generation must be in the `pending` space. -When all objects in the heap have been scanned a cycle ends, and all objects are moved -to the `pending` list again. To avoid having to traverse the entire list, which list is -`pending` and which is `visited` is determined by a field in the `GCState` struct. -The `visited` and `pending` lists can be swapped by toggling this bit. - -Correctness ------------ - -The [algorithm for identifying cycles](#Identifying-reference-cycles) will find all -unreachable cycles in a list of objects, but will not find any cycles that are -even partly outside of that list. -Therefore, to be guaranteed that a full scavenge will find all unreachable cycles, -each cycle must be fully contained within a single increment. - -To make sure that no partial cycles are included in the increment we perform a -[transitive closure](https://en.wikipedia.org/wiki/Transitive_closure) -over reachable, unscanned objects from the initial increment. -Since the transitive closure of objects reachable from an object must be a (non-strict) -superset of any unreachable cycle including that object, we are guaranteed that a -transitive closure cannot contain any partial cycles. -We can exclude scanned objects, as they must have been reachable when scanned. -If a scanned object becomes part of an unreachable cycle after being scanned, it will -not be collected at this time, but it will be collected in the next full scavenge. +three spaces/generations. Every new +object starts in the first generation (generation 0). The previous algorithm is +executed only over the objects of a particular generation and if an object +survives a collection of its generation it will be moved to the next one +(generation 1), where it will be surveyed for collection less often. If +the same object survives another GC round in this new generation (generation 1) +it will be moved to the last generation (generation 2) where it will be +surveyed the least often. > [!NOTE] > The GC implementation for the free-threaded build does not use incremental collection. > Every collection operates on the entire heap. + In order to decide when to run, the collector keeps track of the number of object allocations and deallocations since the last collection. When the number of allocations minus the number of deallocations exceeds `threshold0`, -collection starts. `threshold1` determines the fraction of the old -collection that is included in the increment. -The fraction is inversely proportional to `threshold1`, -as historically a larger `threshold1` meant that old generation -collections were performed less frequently. -`threshold2` is ignored. +collection starts. Initially only generation 0 is examined. If generation 0 has +been examined more than `threshold_1` times since generation 1 has been +examined, then generation 1 is examined as well. With generation 2, +things are a bit more complicated; see +[Collecting the oldest generation](#Collecting-the-oldest-generation) for +more information. These thresholds can be examined using the [`gc.get_threshold()`](https://docs.python.org/3/library/gc.html#gc.get_threshold) @@ -450,7 +404,7 @@ function: ```pycon >>> import gc >>> gc.get_threshold() -(700, 10, 10) +(2000, 10, 10) ``` The content of these generations can be examined using the @@ -463,84 +417,61 @@ specifically in a generation by calling `gc.collect(generation=NUM)`. ... pass ... >>> # Move everything to the old generation so it's easier to inspect ->>> # the young generation. +>>> # the younger generation. >>> gc.collect() 0 >>> # Create a reference cycle. >>> x = MyObj() >>> x.self = x ->>> ->>> # Initially the object is in the young generation. +>>> +>>> # Initially the object is in the youngest generation. >>> gc.get_objects(generation=0) [..., <__main__.MyObj object at 0x7fbcc12a3400>, ...] ->>> +>>> >>> # After a collection of the youngest generation the object ->>> # moves to the old generation. +>>> # moves to the next generation. >>> gc.collect(generation=0) 0 >>> gc.get_objects(generation=0) [] >>> gc.get_objects(generation=1) -[] ->>> gc.get_objects(generation=2) [..., <__main__.MyObj object at 0x7fbcc12a3400>, ...] ``` +Collecting the oldest generation +-------------------------------- + +In addition to the various configurable thresholds, the GC only triggers a full +collection of the oldest generation 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 (that is, 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 +of creating and storing lots of long-lived objects (for example, building a large list +of GC-tracked objects would show quadratic performance, instead of linear as +expected). 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"). + Optimization: excluding reachable objects ========================================= An object cannot be garbage if it can be reached. To avoid having to identify -reference cycles across the whole heap, we can reduce the amount of work done -considerably by first identifying objects reachable from objects known to be -alive. These objects are excluded from the normal cyclic detection process. - -The default and free-threaded build both implement this optimization but in -slightly different ways. - -Finding reachable objects for the default build GC --------------------------------------------------- - -This works by first moving most reachable objects to the `visited` space. -Empirically, most reachable objects can be reached from a small set of global -objects and local variables. This step does much less work per object, so -reduces the time spent performing garbage collection by at least half. - -> [!NOTE] -> Objects that are not determined to be reachable by this pass are not necessarily -> unreachable. We still need to perform the main algorithm to determine which objects -> are actually unreachable. -We use the same technique of forming a transitive closure as the incremental -collector does to find reachable objects, seeding the list with some global -objects and the currently executing frames. - -This phase moves objects to the `visited` space, as follows: - -1. All objects directly referred to by any builtin class, the `sys` module, the `builtins` -module and all objects directly referred to from stack frames are added to a working -set of reachable objects. -2. Until this working set is empty: - 1. Pop an object from the set and move it to the `visited` space - 2. For each object directly reachable from that object: - * If it is not already in `visited` space and it is a GC object, - add it to the working set - - -Before each increment of collection is performed, the stacks are scanned -to check for any new stack frames that have been created since the last -increment. All objects directly referred to from those stack frames are -added to the working set. -Then the above algorithm is repeated, starting from step 2. - +reference cycles across the whole heap, the free-threaded build first identifies +objects reachable from objects known to be alive. These objects are excluded +from the normal cyclic detection process. Finding reachable objects for the free-threaded GC -------------------------------------------------- Within the `gc_free_threading.c` implementation, this is known as the "mark -alive" pass or phase. It is similar in concept to what is done for the default -build GC. Rather than moving objects between double-linked lists, the -free-threaded GC uses a flag in `ob_gc_bits` to track if an object is -found to be definitely alive (not garbage). +alive" pass or phase. The free-threaded GC uses a flag in `ob_gc_bits` to track +if an object is found to be definitely alive (not garbage). To find objects reachable from known alive objects, known as the "roots", the `gc_mark_alive_from_roots()` function is used. Root objects include diff --git a/Lib/test/_test_gc_fast_cycles.py b/Lib/test/_test_gc_fast_cycles.py deleted file mode 100644 index 4e2c7d72a02713..00000000000000 --- a/Lib/test/_test_gc_fast_cycles.py +++ /dev/null @@ -1,48 +0,0 @@ -# Run by test_gc. -from test import support -import _testinternalcapi -import gc -import unittest - -class IncrementalGCTests(unittest.TestCase): - - # Use small increments to emulate longer running process in a shorter time - @support.gc_threshold(200, 10) - def test_incremental_gc_handles_fast_cycle_creation(self): - - class LinkedList: - - #Use slots to reduce number of implicit objects - __slots__ = "next", "prev", "surprise" - - def __init__(self, next=None, prev=None): - self.next = next - if next is not None: - next.prev = self - self.prev = prev - if prev is not None: - prev.next = self - - def make_ll(depth): - head = LinkedList() - for i in range(depth): - head = LinkedList(head, head.prev) - return head - - head = make_ll(1000) - - assert(gc.isenabled()) - olds = [] - initial_heap_size = _testinternalcapi.get_tracked_heap_size() - for i in range(20_000): - newhead = make_ll(20) - newhead.surprise = head - olds.append(newhead) - if len(olds) == 20: - new_objects = _testinternalcapi.get_tracked_heap_size() - initial_heap_size - self.assertLess(new_objects, 27_000, f"Heap growing. Reached limit after {i} iterations") - del olds[:] - - -if __name__ == "__main__": - unittest.main() diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 6aa6361d5d0b92..154d0c980ccaac 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 @@ -419,11 +419,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: @@ -907,10 +915,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() @@ -1249,17 +1289,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/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-11-30-00.gh-issue-142516.GcGen315.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-11-30-00.gh-issue-142516.GcGen315.rst new file mode 100644 index 00000000000000..5abb2485c20f12 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-11-30-00.gh-issue-142516.GcGen315.rst @@ -0,0 +1 @@ +Forward-port the generational cycle garbage collector to the default 3.15 build, replacing the incremental collector while leaving the free-threaded collector unchanged. diff --git a/Modules/_remote_debugging/debug_offsets_validation.h b/Modules/_remote_debugging/debug_offsets_validation.h index 32800e767b3169..f2312aee37f8ce 100644 --- a/Modules/_remote_debugging/debug_offsets_validation.h +++ b/Modules/_remote_debugging/debug_offsets_validation.h @@ -31,7 +31,7 @@ #define FIELD_SIZE(type, member) sizeof(((type *)0)->member) enum { - PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 840, + PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 824, PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE = 104, }; diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index deac8570fe3241..9952669d1c5004 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2718,7 +2718,8 @@ has_deferred_refcount(PyObject *self, PyObject *op) static PyObject * get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored)) { - return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size); + // Generational GC doesn't track heap_size, return -1. + return PyLong_FromInt64(-1); } static PyObject * diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 8da28130e9da9a..e77ca623198c56 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -158,6 +158,15 @@ gc_set_threshold_impl(PyObject *module, int threshold0, int group_right_1, { GCState *gcstate = get_gc_state(); +#ifndef Py_GIL_DISABLED + gcstate->generations[0].threshold = threshold0; + if (group_right_1) { + gcstate->generations[1].threshold = threshold1; + } + if (group_right_2) { + gcstate->generations[2].threshold = threshold2; + } +#else gcstate->young.threshold = threshold0; if (group_right_1) { gcstate->old[0].threshold = threshold1; @@ -165,6 +174,7 @@ gc_set_threshold_impl(PyObject *module, int threshold0, int group_right_1, if (group_right_2) { gcstate->old[1].threshold = threshold2; } +#endif Py_RETURN_NONE; } @@ -179,10 +189,17 @@ gc_get_threshold_impl(PyObject *module) /*[clinic end generated code: output=7902bc9f41ecbbd8 input=286d79918034d6e6]*/ { GCState *gcstate = get_gc_state(); +#ifndef Py_GIL_DISABLED + return Py_BuildValue("(iii)", + gcstate->generations[0].threshold, + gcstate->generations[1].threshold, + gcstate->generations[2].threshold); +#else return Py_BuildValue("(iii)", gcstate->young.threshold, gcstate->old[0].threshold, - 0); + gcstate->old[1].threshold); +#endif } /*[clinic input] @@ -206,10 +223,17 @@ gc_get_count_impl(PyObject *module) gc->alloc_count = 0; #endif +#ifndef Py_GIL_DISABLED + return Py_BuildValue("(iii)", + gcstate->generations[0].count, + gcstate->generations[1].count, + gcstate->generations[2].count); +#else 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); +#endif } /*[clinic input] @@ -347,9 +371,9 @@ gc_get_stats_impl(PyObject *module) /* To get consistent values despite allocations while constructing the result list, we use a snapshot of the running stats. */ GCState *gcstate = get_gc_state(); - stats[0] = gcstate->generation_stats->young.items[gcstate->generation_stats->young.index]; - stats[1] = gcstate->generation_stats->old[0].items[gcstate->generation_stats->old[0].index]; - stats[2] = gcstate->generation_stats->old[1].items[gcstate->generation_stats->old[1].index]; + stats[0] = gcstate->generation_stats[0]; + stats[1] = gcstate->generation_stats[1]; + stats[2] = gcstate->generation_stats[2]; PyObject *result = PyList_New(0); if (result == NULL) diff --git a/Python/gc.c b/Python/gc.c index 284ac725d37ac6..658a5e94dfde24 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -6,17 +6,18 @@ #include "pycore_ceval.h" // _Py_set_eval_breaker_bit() #include "pycore_dict.h" // _PyInlineValuesSize() #include "pycore_initconfig.h" // _PyStatus_OK() +#include "pycore_context.h" #include "pycore_interp.h" // PyInterpreterState.gc #include "pycore_interpframe.h" // _PyFrame_GetLocalsArray() +#include "pycore_object.h" #include "pycore_object_alloc.h" // _PyObject_MallocWithType() +#include "pycore_pyerrors.h" #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_tuple.h" // _PyTuple_MaybeUntrack() #include "pycore_weakref.h" // _PyWeakref_ClearRef() - #include "pydtrace.h" - -#ifndef Py_GIL_DISABLED +#if !defined(Py_GIL_DISABLED) typedef struct _gc_runtime_state GCState; @@ -24,10 +25,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,48 +97,9 @@ 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; -} +#define GEN_HEAD(gcstate, n) (&(gcstate)->generations[n].head) -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; -} - -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; - case 2: - return &gcstate->old[gcstate->visited_space^1].head; - default: - Py_UNREACHABLE(); - } -} static GCState * get_gc_state(void) @@ -160,12 +118,11 @@ _PyGC_InitState(GCState *gcstate) GEN.head._gc_prev = (uintptr_t)&GEN.head; \ } while (0) - assert(gcstate->young.count == 0); - assert(gcstate->old[0].count == 0); - assert(gcstate->old[1].count == 0); - INIT_HEAD(gcstate->young); - INIT_HEAD(gcstate->old[0]); - INIT_HEAD(gcstate->old[1]); + for (int i = 0; i < NUM_GENERATIONS; i++) { + assert(gcstate->generations[i].count == 0); + INIT_HEAD(gcstate->generations[i]); + }; + gcstate->generation0 = GEN_HEAD(gcstate, 0); INIT_HEAD(gcstate->permanent_generation); #undef INIT_HEAD @@ -177,11 +134,6 @@ _PyGC_Init(PyInterpreterState *interp) { GCState *gcstate = &interp->gc; - gcstate->generation_stats = PyMem_RawCalloc(1, sizeof(struct gc_stats)); - if (gcstate->generation_stats == NULL) { - return _PyStatus_NO_MEMORY(); - } - gcstate->garbage = PyList_New(0); if (gcstate->garbage == NULL) { return _PyStatus_NO_MEMORY(); @@ -191,7 +143,6 @@ _PyGC_Init(PyInterpreterState *interp) if (gcstate->callbacks == NULL) { return _PyStatus_NO_MEMORY(); } - gcstate->heap_size = 0; return _PyStatus_OK(); } @@ -269,7 +220,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 @@ -327,8 +277,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); @@ -397,8 +345,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: @@ -420,7 +368,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); @@ -430,58 +378,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 ***/ @@ -501,8 +401,8 @@ update_refs(PyGC_Head *containers) if (_Py_IsImmortal(op)) { assert(!_Py_IsStaticImmortal(op)); _PyObject_GC_UNTRACK(op); - gc = next; - continue; + gc = next; + continue; } gc_reset_refs(gc, Py_REFCNT(op)); /* Python's cyclic gc should never see an incoming refcount @@ -530,19 +430,12 @@ update_refs(PyGC_Head *containers) return candidates; } -struct visit_decref_context { - PyObject *parent; - struct gc_generation_stats *stats; -}; - /* A traversal callback for subtract_refs. */ static int -visit_decref(PyObject *op, void *arg) +visit_decref(PyObject *op, void *parent) { OBJECT_STAT_INC(object_visits); - struct visit_decref_context *ctx = (struct visit_decref_context *)arg; - ctx->stats->object_visits += 1; - _PyObject_ASSERT(ctx->parent, !_PyObject_IsFreed(op)); + _PyObject_ASSERT(_PyObject_CAST(parent), !_PyObject_IsFreed(op)); if (_PyObject_IS_GC(op)) { PyGC_Head *gc = AS_GC(op); @@ -589,35 +482,24 @@ _PyGC_VisitFrameStack(_PyInterpreterFrame *frame, visitproc visit, void *arg) * reachable from outside containers, and so can't be collected. */ static void -subtract_refs(PyGC_Head *containers, struct gc_generation_stats *stats) +subtract_refs(PyGC_Head *containers) { traverseproc traverse; PyGC_Head *gc = GC_NEXT(containers); for (; gc != containers; gc = GC_NEXT(gc)) { PyObject *op = FROM_GC(gc); traverse = Py_TYPE(op)->tp_traverse; - struct visit_decref_context ctx = { - .parent = op, - .stats = stats - }; (void) traverse(op, visit_decref, - &ctx); + op); } } -struct visit_reachable_context { - PyGC_Head *head; - struct gc_generation_stats *stats; -}; - /* A traversal callback for move_unreachable. */ static int visit_reachable(PyObject *op, void *arg) { - struct visit_reachable_context *ctx = (struct visit_reachable_context *)arg; - ctx->stats->object_visits += 1; - PyGC_Head *reachable = ctx->head; + PyGC_Head *reachable = arg; OBJECT_STAT_INC(object_visits); if (!_PyObject_IS_GC(op)) { return 0; @@ -647,13 +529,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); @@ -690,7 +571,7 @@ visit_reachable(PyObject *op, void *arg) * So we can not gc_list_* functions for unreachable until we remove the flag. */ static void -move_unreachable(PyGC_Head *young, PyGC_Head *unreachable, struct gc_generation_stats *stats) +move_unreachable(PyGC_Head *young, PyGC_Head *unreachable) { // previous elem in the young list, used for restore gc_prev. PyGC_Head *prev = young; @@ -705,14 +586,6 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable, struct gc_generation_ * or to the right have been scanned yet. */ - struct visit_reachable_context ctx = { - .head = young, - .stats = stats - }; - - 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 @@ -731,7 +604,7 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable, struct gc_generation_ // young->_gc_prev == gc. Don't do gc = GC_NEXT(gc) before! (void) traverse(op, visit_reachable, - &ctx); + (void *)young); // relink gc_prev to prev element. _PyGCHead_SET_PREV(gc, prev); // gc is not COLLECTING state after here. @@ -758,18 +631,17 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable, struct gc_generation_ // 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 @@ -825,8 +697,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); @@ -849,8 +721,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); } @@ -859,9 +731,7 @@ clear_unreachable_mask(PyGC_Head *unreachable) static int visit_move(PyObject *op, void *arg) { - struct visit_reachable_context *ctx = (struct visit_reachable_context *)arg; - PyGC_Head *tolist = ctx->head; - ctx->stats->object_visits += 1; + PyGC_Head *tolist = arg; OBJECT_STAT_INC(object_visits); if (_PyObject_IS_GC(op)) { PyGC_Head *gc = AS_GC(op); @@ -877,12 +747,8 @@ visit_move(PyObject *op, void *arg) * into finalizers set. */ static void -move_legacy_finalizer_reachable(PyGC_Head *finalizers, struct gc_generation_stats *stats) +move_legacy_finalizer_reachable(PyGC_Head *finalizers) { - struct visit_reachable_context ctx = { - .head = finalizers, - .stats = stats - }; traverseproc traverse; PyGC_Head *gc = GC_NEXT(finalizers); for (; gc != finalizers; gc = GC_NEXT(gc)) { @@ -890,7 +756,7 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers, struct gc_generation_stat traverse = Py_TYPE(FROM_GC(gc))->tp_traverse; (void) traverse(FROM_GC(gc), visit_move, - &ctx); + (void *)finalizers); } } @@ -1035,7 +901,6 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old) /* 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; @@ -1071,7 +936,6 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old) 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 { @@ -1250,6 +1114,25 @@ delete_garbage(PyThreadState *tstate, GCState *gcstate, } +// Show stats for objects in each generations +static void +show_stats_each_generations(GCState *gcstate) +{ + char buf[100]; + size_t pos = 0; + + for (int i = 0; i < NUM_GENERATIONS && pos < sizeof(buf); i++) { + pos += PyOS_snprintf(buf+pos, sizeof(buf)-pos, + " %zd", + gc_list_size(GEN_HEAD(gcstate, i))); + } + + PySys_FormatStderr( + "gc: objects in each generation:%s\n" + "gc: objects in permanent generation: %zd\n", + buf, gc_list_size(&gcstate->permanent_generation.head)); +} + /* Deduce which objects among "base" are unreachable from outside the list and move them to 'unreachable'. The process consist in the following steps: @@ -1278,7 +1161,7 @@ flag is cleared (for example, by using 'clear_unreachable_mask' function or by a call to 'move_legacy_finalizers'), the 'unreachable' list is not a normal list and we can not use most gc_list_* functions for it. */ static inline Py_ssize_t -deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable, struct gc_generation_stats *stats) { +deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) { validate_list(base, collecting_clear_unreachable_clear); /* Using ob_refcnt and gc_refs, calculate which objects in the * container set are reachable from outside the set (i.e., have a @@ -1286,7 +1169,7 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable, struct gc_generation * set are taken into account). */ Py_ssize_t candidates = update_refs(base); // gc_prev is used for gc_refs - subtract_refs(base, stats); + subtract_refs(base); /* Leave everything reachable from outside base in base, and move * everything else (in base) to unreachable. @@ -1323,7 +1206,8 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable, struct gc_generation * the reachable objects instead. But this is a one-time cost, probably not * worth complicating the code to speed just a little. */ - move_unreachable(base, unreachable, stats); // gc_prev is pointer again + gc_list_init(unreachable); + move_unreachable(base, unreachable); // gc_prev is pointer again validate_list(base, collecting_clear_unreachable_clear); validate_list(unreachable, collecting_set_unreachable_set); return candidates; @@ -1344,8 +1228,7 @@ PREV_MARK_COLLECTING set, but the objects in this set are going to be removed so we can skip the expense of clearing the flag to avoid extra iteration. */ static inline void handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, - PyGC_Head *old_generation, - struct gc_generation_stats *stats) + PyGC_Head *old_generation) { // Remove the PREV_MASK_COLLECTING from unreachable // to prepare it for a new call to 'deduce_unreachable' @@ -1355,507 +1238,239 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, // have the PREV_MARK_COLLECTING set, but the objects are going to be // removed so we can skip the expense of clearing the flag. PyGC_Head* resurrected = unreachable; - deduce_unreachable(resurrected, still_unreachable, stats); + deduce_unreachable(resurrected, still_unreachable); clear_unreachable_mask(still_unreachable); // Move the resurrected objects to the old generation for future collection. gc_list_merge(resurrected, old_generation); } -static void -gc_collect_region(PyThreadState *tstate, - PyGC_Head *from, - PyGC_Head *to, - struct gc_generation_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) +/* Invoke progress callbacks to notify clients that garbage collection + * is starting or stopping */ - -/* 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 struct gc_generation_stats * -gc_get_stats(GCState *gcstate, int gen) +static void +invoke_gc_callback(PyThreadState *tstate, const char *phase, + int generation, struct gc_collection_stats *stats) { - if (gen == 0) { - struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young; - buffer->index = (buffer->index + 1) % GC_YOUNG_STATS_SIZE; - struct gc_generation_stats *stats = &buffer->items[buffer->index]; - return stats; + assert(!_PyErr_Occurred(tstate)); + + /* we may get called very early */ + GCState *gcstate = &tstate->interp->gc; + if (gcstate->callbacks == NULL) { + return; } - else { - struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1]; - buffer->index = (buffer->index + 1) % GC_OLD_STATS_SIZE; - struct gc_generation_stats *stats = &buffer->items[buffer->index]; - return stats; + + /* The local variable cannot be rebound, check it for sanity */ + assert(PyList_CheckExact(gcstate->callbacks)); + PyObject *info = NULL; + if (PyList_GET_SIZE(gcstate->callbacks) != 0) { + info = Py_BuildValue("{sisnsnsnsd}", + "generation", generation, + "collected", stats->collected, + "uncollectable", stats->uncollectable, + "candidates", stats->candidates, + "duration", stats->duration); + if (info == NULL) { + PyErr_FormatUnraisable("Exception ignored on invoking gc callbacks"); + return; + } } -} -static struct gc_generation_stats * -gc_get_prev_stats(GCState *gcstate, int gen) -{ - if (gen == 0) { - struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young; - struct gc_generation_stats *stats = &buffer->items[buffer->index]; - return stats; + PyObject *phase_obj = PyUnicode_FromString(phase); + if (phase_obj == NULL) { + Py_XDECREF(info); + PyErr_FormatUnraisable("Exception ignored on invoking gc callbacks"); + return; } - else { - struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1]; - struct gc_generation_stats *stats = &buffer->items[buffer->index]; - return stats; + + PyObject *stack[] = {phase_obj, info}; + for (Py_ssize_t i=0; icallbacks); i++) { + PyObject *r, *cb = PyList_GET_ITEM(gcstate->callbacks, i); + Py_INCREF(cb); /* make sure cb doesn't go away */ + r = PyObject_Vectorcall(cb, stack, 2, NULL); + if (r == NULL) { + PyErr_FormatUnraisable("Exception ignored while " + "calling GC callback %R", cb); + } + else { + Py_DECREF(r); + } + Py_DECREF(cb); } + Py_DECREF(phase_obj); + Py_XDECREF(info); + assert(!_PyErr_Occurred(tstate)); } -static void -add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats) -{ - struct gc_generation_stats *prev_stats = gc_get_prev_stats(gcstate, gen); - struct gc_generation_stats *cur_stats = gc_get_stats(gcstate, gen); - - memcpy(cur_stats, prev_stats, sizeof(struct gc_generation_stats)); - - cur_stats->ts_start = stats->ts_start; - cur_stats->ts_stop = stats->ts_stop; - cur_stats->heap_size = stats->heap_size; - cur_stats->work_to_do = stats->work_to_do; - - cur_stats->collections += 1; - cur_stats->object_visits += stats->object_visits; - cur_stats->collected += stats->collected; - cur_stats->uncollectable += stats->uncollectable; - cur_stats->candidates += stats->candidates; - - cur_stats->objects_transitively_reachable += stats->objects_transitively_reachable; - cur_stats->objects_not_transitively_reachable += stats->objects_not_transitively_reachable; - cur_stats->duration += stats->duration; +/* 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 +gc_select_generation(GCState *gcstate) +{ + for (int i = NUM_GENERATIONS-1; i >= 0; i--) { + if (gcstate->generations[i].count > gcstate->generations[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 -1; } static void -gc_collect_young(PyThreadState *tstate, - struct gc_generation_stats *stats) +add_stats(GCState *gcstate, int gen, 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); - - 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++; - validate_spaces(gcstate); + gcstate->generation_stats[gen].duration += stats->duration; + gcstate->generation_stats[gen].collected += stats->collected; + gcstate->generation_stats[gen].uncollectable += stats->uncollectable; + gcstate->generation_stats[gen].candidates += stats->candidates; + gcstate->generation_stats[gen].collections += 1; } -#ifndef NDEBUG -static inline int -IS_IN_VISITED(PyGC_Head *gc, int visited_space) +/* This is the main function. Read this to understand how the + * collection process works. */ +static Py_ssize_t +gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) { - assert(visited_space == 0 || other_space(visited_space) == 0); - return gc_old_space(gc) == visited_space; -} -#endif + int i; + PyGC_Head *young; /* the generation we are examining */ + PyGC_Head *old; /* next older generation */ + PyGC_Head unreachable; /* non-problematic unreachable trash */ + PyGC_Head finalizers; /* objects with, & reachable from, __del__ */ + PyGC_Head *gc; + PyTime_t t1 = 0; /* initialize to prevent a compiler warning */ + GCState *gcstate = &tstate->interp->gc; -struct container_and_flag { - PyGC_Head *container; - int visited_space; - intptr_t size; - struct gc_generation_stats *stats; -}; + // gc_collect_main() must not be called before _PyGC_Init + // or after _PyGC_Fini() + assert(gcstate->garbage != NULL); + assert(!_PyErr_Occurred(tstate)); -/* A traversal callback for adding to container) */ -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; - cf->stats->object_visits += 1; - 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++; - } + int expected = 0; + if (!_Py_atomic_compare_exchange_int(&gcstate->collecting, &expected, 1)) { + // Don't start a garbage collection if one is already in progress. + return 0; } - return 0; -} -static intptr_t -expand_region_transitively_reachable(PyGC_Head *container, - PyGC_Head *gc, - GCState *gcstate, - struct gc_generation_stats *stats) -{ - struct container_and_flag arg = { - .container = container, - .visited_space = gcstate->visited_space, - .size = 0, - .stats = stats - }; - 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, &gcstate->permanent_generation.head); - gc = next; - continue; + 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. + _Py_atomic_store_int(&gcstate->collecting, 0); + return 0; } - 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); -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; - } +#ifdef Py_STATS + if (_Py_stats) { + _Py_stats->object_stats.object_visits = 0; } - return 0; -} +#endif + GC_STAT_ADD(generation, collections, 1); -static intptr_t -mark_all_reachable(PyGC_Head *reachable, PyGC_Head *visited, int visited_space, struct gc_generation_stats *stats) -{ - // Transitively traverse all objects from reachable, until empty - struct container_and_flag arg = { - .container = reachable, - .visited_space = visited_space, - .size = 0, - .stats = stats - }; - 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, struct gc_generation_stats *stats) -{ - 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); + struct gc_collection_stats stats = { 0 }; + if (reason != _Py_GC_REASON_SHUTDOWN) { + invoke_gc_callback(tstate, "start", generation, &stats); } - objects_marked += mark_all_reachable(&reachable, visited, visited_space, stats); - assert(gc_list_is_empty(&reachable)); - return objects_marked; -} -static intptr_t -mark_global_roots(PyInterpreterState *interp, PyGC_Head *visited, int visited_space, struct gc_generation_stats *stats) -{ - 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); + // ignore error: don't interrupt the GC if reading the clock fails + (void)PyTime_PerfCounterRaw(&t1); + if (gcstate->debug & _PyGC_DEBUG_STATS) { + PySys_WriteStderr("gc: collecting generation %d...\n", generation); + show_stats_each_generations(gcstate); } - 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); + + if (PyDTrace_GC_START_ENABLED()) { + PyDTrace_GC_START(generation); } - objects_marked += mark_all_reachable(&reachable, visited, visited_space, stats); - assert(gc_list_is_empty(&reachable)); - return objects_marked; -} -static intptr_t -mark_at_start(PyThreadState *tstate, struct gc_generation_stats *stats) -{ - // 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, stats); - objects_marked += mark_stacks(tstate->interp, visited, gcstate->visited_space, true, stats); - 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; + /* update collection and allocation counters */ + if (generation+1 < NUM_GENERATIONS) { + gcstate->generations[generation+1].count += 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; + for (i = 0; i <= generation; i++) { + gcstate->generations[i].count = 0; } - gcstate->young.count = 0; - return new_objects + heap_fraction; -} -static void -gc_collect_increment(PyThreadState *tstate, struct gc_generation_stats *stats) -{ - GCState *gcstate = &tstate->interp->gc; - gcstate->work_to_do += assess_work_to_do(gcstate); - if (gcstate->work_to_do < 0) { - return; + /* 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)); } - untrack_tuples(&gcstate->young.head); - if (gcstate->phase == GC_PHASE_MARK) { - Py_ssize_t objects_marked = mark_at_start(tstate, stats); - stats->objects_transitively_reachable += objects_marked; - stats->candidates += objects_marked; - gcstate->work_to_do -= objects_marked; - validate_spaces(gcstate); - return; + + /* handy references */ + young = GEN_HEAD(gcstate, generation); + if (generation < NUM_GENERATIONS-1) { + old = GEN_HEAD(gcstate, generation+1); } - 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, stats); - stats->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, stats); + else { + old = young; } - stats->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; + validate_list(old, collecting_clear_unreachable_clear); - if (gc_list_is_empty(not_visited)) { - completed_scavenge(gcstate); - } - validate_spaces(gcstate); -} + stats.candidates = deduce_unreachable(young, &unreachable); -static void -gc_collect_full(PyThreadState *tstate, - struct gc_generation_stats *stats) -{ - 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); -} - -/* This is the main function. Read this to understand how the - * collection process works. */ -static void -gc_collect_region(PyThreadState *tstate, - PyGC_Head *from, - PyGC_Head *to, - struct gc_generation_stats *stats) -{ - PyGC_Head unreachable; /* non-problematic unreachable trash */ - PyGC_Head finalizers; /* objects with, & reachable from, __del__ */ - PyGC_Head *gc; /* initialize to prevent a compiler warning */ - GCState *gcstate = &tstate->interp->gc; - - assert(gcstate->garbage != NULL); - assert(!_PyErr_Occurred(tstate)); - - gc_list_init(&unreachable); - stats->candidates = deduce_unreachable(from, &unreachable, stats); - validate_consistent_old_space(from); - untrack_tuples(from); - - /* Move reachable objects to next generation. */ - validate_consistent_old_space(to); - if (from != to) { - gc_list_merge(from, to); + /* Move reachable objects to next generation. */ + if (young != old) { + if (generation == NUM_GENERATIONS - 2) { + gcstate->long_lived_pending += gc_list_size(young); + } + gc_list_merge(young, old); + } + 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(young); } - validate_consistent_old_space(to); /* All objects in unreachable are trash, but objects reachable from * legacy finalizers (e.g. tp_del) can't safely be deleted. @@ -1868,9 +1483,11 @@ gc_collect_region(PyThreadState *tstate, * unreachable objects reachable *from* those are also uncollectable, * and we move those into the finalizers list too. */ - move_legacy_finalizer_reachable(&finalizers, stats); + move_legacy_finalizer_reachable(&finalizers); + validate_list(&finalizers, collecting_clear_unreachable_clear); validate_list(&unreachable, collecting_set_unreachable_clear); + /* Print debugging information. */ if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) { for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) { @@ -1878,23 +1495,25 @@ gc_collect_region(PyThreadState *tstate, } } - /* Invoke weakref callbacks as necessary. */ - stats->collected += handle_weakref_callbacks(&unreachable, to); - gc_list_validate_space(to, gcstate->visited_space); - validate_list(to, collecting_clear_unreachable_clear); + /* Clear weakrefs and invoke callbacks as necessary. */ + stats.collected += handle_weakref_callbacks(&unreachable, old); + validate_list(old, collecting_clear_unreachable_clear); validate_list(&unreachable, collecting_set_unreachable_clear); /* Call tp_finalize on objects which have one. */ finalize_garbage(tstate, &unreachable); + /* Handle any objects that may have resurrected after the call * to 'finalize_garbage' and continue the collection with the * objects that are still unreachable */ PyGC_Head final_unreachable; - gc_list_init(&final_unreachable); - handle_resurrected_objects(&unreachable, &final_unreachable, to, stats); + handle_resurrected_objects(&unreachable, &final_unreachable, old); - /* Clear weakrefs to objects in the unreachable set. See the comments - * above handle_weakref_callbacks() for details. + /* Clear weakrefs to objects in the unreachable set. No Python-level + * code must be allowed to access those unreachable objects. During + * delete_garbage(), finalizers outside the unreachable set might run + * and create new weakrefs. If those weakrefs were not cleared, they + * could reveal unreachable objects. Callbacks are not executed. */ clear_weakrefs(&final_unreachable); @@ -1902,8 +1521,8 @@ gc_collect_region(PyThreadState *tstate, * the reference cycles to be broken. It may also cause some objects * in finalizers to be freed. */ - stats->collected += gc_list_size(&final_unreachable); - delete_garbage(tstate, gcstate, &final_unreachable, to); + stats.collected += gc_list_size(&final_unreachable); + delete_garbage(tstate, gcstate, &final_unreachable, old); /* Collect statistics on uncollectable objects found and print * debugging information. */ @@ -1913,75 +1532,61 @@ gc_collect_region(PyThreadState *tstate, if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) debug_cycle("uncollectable", FROM_GC(gc)); } - stats->uncollectable = n; + stats.uncollectable = n; + PyTime_t t2; + (void)PyTime_PerfCounterRaw(&t2); + stats.duration = PyTime_AsSecondsDouble(t2 - t1); + if (gcstate->debug & _PyGC_DEBUG_STATS) { + PySys_WriteStderr( + "gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n", + stats.uncollectable+stats.collected, stats.uncollectable, + stats.duration); + } + /* Append instances in the uncollectable set to a Python * reachable list of garbage. The programmer has to deal with * 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); -} + handle_legacy_finalizers(tstate, gcstate, &finalizers, old); + validate_list(old, collecting_clear_unreachable_clear); -/* Invoke progress callbacks to notify clients that garbage collection - * is starting or stopping - */ -static void -do_gc_callback(GCState *gcstate, const char *phase, - int generation, struct gc_generation_stats *stats) -{ - assert(!PyErr_Occurred()); + /* Clear free list only during the collection of the highest + * generation */ + if (generation == NUM_GENERATIONS-1) { + _PyGC_ClearAllFreeLists(tstate->interp); + } - /* The local variable cannot be rebound, check it for sanity */ - assert(PyList_CheckExact(gcstate->callbacks)); - PyObject *info = NULL; - if (PyList_GET_SIZE(gcstate->callbacks) != 0) { - info = Py_BuildValue("{sisnsnsnsd}", - "generation", generation, - "collected", stats->collected, - "uncollectable", stats->uncollectable, - "candidates", stats->candidates, - "duration", stats->duration); - if (info == NULL) { - PyErr_FormatUnraisable("Exception ignored while invoking gc callbacks"); - return; + if (_PyErr_Occurred(tstate)) { + if (reason == _Py_GC_REASON_SHUTDOWN) { + _PyErr_Clear(tstate); + } + else { + PyErr_FormatUnraisable("Exception ignored in garbage collection"); } } - PyObject *phase_obj = PyUnicode_FromString(phase); - if (phase_obj == NULL) { - Py_XDECREF(info); - PyErr_FormatUnraisable("Exception ignored while invoking gc callbacks"); - return; + /* Update stats */ + add_stats(gcstate, generation, &stats); + GC_STAT_ADD(generation, objects_collected, m); +#ifdef Py_STATS + if (_Py_stats) { + GC_STAT_ADD(generation, object_visits, + _Py_stats->object_stats.object_visits); + _Py_stats->object_stats.object_visits = 0; } +#endif - PyObject *stack[] = {phase_obj, info}; - for (Py_ssize_t i=0; icallbacks); i++) { - PyObject *r, *cb = PyList_GET_ITEM(gcstate->callbacks, i); - Py_INCREF(cb); /* make sure cb doesn't go away */ - r = PyObject_Vectorcall(cb, stack, 2, NULL); - if (r == NULL) { - PyErr_FormatUnraisable("Exception ignored while " - "calling GC callback %R", cb); - } - else { - Py_DECREF(r); - } - Py_DECREF(cb); + if (PyDTrace_GC_DONE_ENABLED()) { + PyDTrace_GC_DONE(stats.uncollectable + stats.collected); } - Py_DECREF(phase_obj); - Py_XDECREF(info); - assert(!PyErr_Occurred()); -} -static void -invoke_gc_callback(GCState *gcstate, const char *phase, - int generation, struct gc_generation_stats *stats) -{ - if (gcstate->callbacks == NULL) { - return; + if (reason != _Py_GC_REASON_SHUTDOWN) { + invoke_gc_callback(tstate, "stop", generation, &stats); } - do_gc_callback(gcstate, phase, generation, stats); + + assert(!_PyErr_Occurred(tstate)); + _Py_atomic_store_int(&gcstate->collecting, 0); + return stats.uncollectable + stats.collected; } static int @@ -2043,25 +1648,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; } } @@ -2076,23 +1676,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); + for (int i = 0; i < NUM_GENERATIONS; ++i) { + gc_list_merge(GEN_HEAD(gcstate, i), &gcstate->permanent_generation.head); + gcstate->generations[i].count = 0; } - else { - gc_list_set_space(old1, 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 @@ -2100,8 +1687,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 @@ -2137,103 +1723,29 @@ PyGC_IsEnabled(void) return gcstate->enabled; } -// Show stats for objects in each generations -static void -show_stats_each_generations(GCState *gcstate) -{ - char buf[100]; - size_t pos = 0; - - for (int i = 0; i < NUM_GENERATIONS && pos < sizeof(buf); i++) { - pos += PyOS_snprintf(buf+pos, sizeof(buf)-pos, - " %zd", - gc_list_size(GEN_HEAD(gcstate, i))); - } - PySys_FormatStderr( - "gc: objects in each generation:%s\n" - "gc: objects in permanent generation: %zd\n", - buf, gc_list_size(&gcstate->permanent_generation.head)); -} - +/* Public API to invoke gc.collect() from C */ Py_ssize_t -_PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason) +PyGC_Collect(void) { + PyThreadState *tstate = _PyThreadState_GET(); GCState *gcstate = &tstate->interp->gc; - assert(tstate->current_frame == NULL || tstate->current_frame->stackpointer != NULL); - int expected = 0; - if (!_Py_atomic_compare_exchange_int(&gcstate->collecting, &expected, 1)) { - // Don't start a garbage collection if one is already in progress. + if (!gcstate->enabled) { return 0; } - gcstate->frame = tstate->current_frame; - struct gc_generation_stats stats = { 0 }; - if (reason != _Py_GC_REASON_SHUTDOWN) { - invoke_gc_callback(gcstate, "start", generation, &stats); - } - if (gcstate->debug & _PyGC_DEBUG_STATS) { - PySys_WriteStderr("gc: collecting generation %d...\n", generation); - show_stats_each_generations(gcstate); - } - if (PyDTrace_GC_START_ENABLED()) { - PyDTrace_GC_START(generation); - } - stats.heap_size = gcstate->heap_size; - stats.work_to_do = gcstate->work_to_do; - (void)PyTime_PerfCounterRaw(&stats.ts_start); + Py_ssize_t n; 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(); - } - (void)PyTime_PerfCounterRaw(&stats.ts_stop); - stats.duration = PyTime_AsSecondsDouble(stats.ts_stop - stats.ts_start); - add_stats(gcstate, generation, &stats); - if (PyDTrace_GC_DONE_ENABLED()) { - PyDTrace_GC_DONE(stats.uncollectable + stats.collected); - } - if (reason != _Py_GC_REASON_SHUTDOWN) { - invoke_gc_callback(gcstate, "stop", generation, &stats); - } + n = gc_collect_main(tstate, NUM_GENERATIONS - 1, _Py_GC_REASON_MANUAL); _PyErr_SetRaisedException(tstate, exc); - GC_STAT_ADD(generation, objects_collected, stats.collected); -#ifdef Py_STATS - PyStats *s = _PyStats_GET(); - if (s) { - GC_STAT_ADD(generation, object_visits, - s->object_stats.object_visits); - s->object_stats.object_visits = 0; - } -#endif - validate_spaces(gcstate); - gcstate->frame = NULL; - _Py_atomic_store_int(&gcstate->collecting, 0); - if (gcstate->debug & _PyGC_DEBUG_STATS) { - PySys_WriteStderr( - "gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n", - stats.collected + stats.uncollectable, stats.uncollectable, stats.duration - ); - } - - return stats.uncollectable + stats.collected; + return n; } -/* Public API to invoke gc.collect() from C */ Py_ssize_t -PyGC_Collect(void) +_PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason) { - return _PyGC_Collect(_PyThreadState_GET(), 2, _Py_GC_REASON_MANUAL); + return gc_collect_main(tstate, generation, reason); } void @@ -2245,7 +1757,7 @@ _PyGC_CollectNoFail(PyThreadState *tstate) during interpreter shutdown (and then never finish it). See http://bugs.python.org/issue8713#msg195178 for an example. */ - _PyGC_Collect(_PyThreadState_GET(), 2, _Py_GC_REASON_SHUTDOWN); + gc_collect_main(tstate, NUM_GENERATIONS - 1, _Py_GC_REASON_SHUTDOWN); } void @@ -2304,8 +1816,6 @@ _PyGC_Fini(PyInterpreterState *interp) GCState *gcstate = &interp->gc; Py_CLEAR(gcstate->garbage); Py_CLEAR(gcstate->callbacks); - PyMem_RawFree(gcstate->generation_stats); - gcstate->generation_stats = NULL; /* Prevent a subtle bug that affects sub-interpreters that use basic * single-phase init extensions (m_size == -1). Those extensions cause objects @@ -2322,9 +1832,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(&gcstate->generations[i].head); + } finalize_unlink_gc_head(&gcstate->permanent_generation.head); } @@ -2398,36 +1908,36 @@ _Py_ScheduleGC(PyThreadState *tstate) } } -void -_Py_TriggerGC(struct _gc_runtime_state *gcstate) -{ - PyThreadState *tstate = _PyThreadState_GET(); - if (gcstate->enabled && - gcstate->young.threshold != 0 && - !_Py_atomic_load_int_relaxed(&gcstate->collecting) && - !_PyErr_Occurred(tstate)) - { - _Py_ScheduleGC(tstate); - } -} - void _PyObject_GC_Link(PyObject *op) { PyGC_Head *gc = AS_GC(op); // gc must be correctly aligned _PyObject_ASSERT(op, ((uintptr_t)gc & (sizeof(uintptr_t)-1)) == 0); + + PyThreadState *tstate = _PyThreadState_GET(); + GCState *gcstate = &tstate->interp->gc; gc->_gc_next = 0; gc->_gc_prev = 0; - + gcstate->generations[0].count++; /* number of allocated GC objects */ + if (gcstate->generations[0].count > gcstate->generations[0].threshold && + gcstate->enabled && + gcstate->generations[0].threshold && + !_Py_atomic_load_int_relaxed(&gcstate->collecting) && + !_PyErr_Occurred(tstate)) + { + _Py_ScheduleGC(tstate); + } } void _Py_RunGC(PyThreadState *tstate) { - if (tstate->interp->gc.enabled) { - _PyGC_Collect(tstate, 1, _Py_GC_REASON_HEAP); + GCState *gcstate = get_gc_state(); + if (!gcstate->enabled) { + return; } + gc_collect_main(tstate, GENERATION_AUTO, _Py_GC_REASON_HEAP); } static PyObject * @@ -2528,11 +2038,6 @@ PyObject_GC_Del(void *op) PyGC_Head *g = AS_GC(op); if (_PyObject_GC_IS_TRACKED(op)) { gc_list_remove(g); - GCState *gcstate = get_gc_state(); - if (gcstate->young.count > 0) { - gcstate->young.count--; - } - gcstate->heap_size--; #ifdef Py_DEBUG PyObject *exc = PyErr_GetRaisedException(); if (PyErr_WarnExplicitFormat(PyExc_ResourceWarning, "gc", 0, @@ -2546,6 +2051,10 @@ PyObject_GC_Del(void *op) PyErr_SetRaisedException(exc); #endif } + GCState *gcstate = get_gc_state(); + if (gcstate->generations[0].count > 0) { + gcstate->generations[0].count--; + } PyObject_Free(((char *)op)-presize); } @@ -2590,18 +2099,14 @@ PyUnstable_GC_VisitObjects(gcvisitobjects_t callback, void *arg) GCState *gcstate = get_gc_state(); int original_state = gcstate->enabled; gcstate->enabled = 0; - if (visit_generation(callback, arg, &gcstate->young) < 0) { - goto done; - } - if (visit_generation(callback, arg, &gcstate->old[0]) < 0) { - goto done; - } - if (visit_generation(callback, arg, &gcstate->old[1]) < 0) { - goto done; + for (size_t i = 0; i < NUM_GENERATIONS; i++) { + if (visit_generation(callback, arg, &gcstate->generations[i]) < 0) { + goto done; + } } visit_generation(callback, arg, &gcstate->permanent_generation); done: gcstate->enabled = original_state; } -#endif // Py_GIL_DISABLED +#endif // !Py_GIL_DISABLED diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 4b46ca04f56b20..6c6218b41839b3 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -1698,11 +1698,6 @@ _PyGC_Init(PyInterpreterState *interp) { GCState *gcstate = &interp->gc; - gcstate->generation_stats = PyMem_RawCalloc(1, sizeof(struct gc_stats)); - if (gcstate->generation_stats == NULL) { - return _PyStatus_NO_MEMORY(); - } - gcstate->garbage = PyList_New(0); if (gcstate->garbage == NULL) { return _PyStatus_NO_MEMORY();