mirror of
https://github.com/python/cpython.git
synced 2025-08-04 00:48:58 +00:00
[3.13] GH-124567: Revert the Incremental GC in 3.13 (#124770)
Revert the incremental GC in 3.13, since it's not clear that without further turning, the benefits outweigh the costs. Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
This commit is contained in:
parent
bc1fae89af
commit
e0eb44ad49
11 changed files with 5525 additions and 5908 deletions
10098
Doc/data/python3.13.abi
10098
Doc/data/python3.13.abi
File diff suppressed because it is too large
Load diff
|
@ -40,18 +40,11 @@ The :mod:`gc` module provides the following functions:
|
||||||
|
|
||||||
.. function:: collect(generation=2)
|
.. 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
|
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
|
:exc:`ValueError` is raised if the generation number is invalid. The sum of
|
||||||
collected objects and uncollectable objects is returned.
|
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
|
The free lists maintained for a number of built-in types are cleared
|
||||||
whenever a full collection or collection of the highest generation (2)
|
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
|
is run. Not all items in some free lists may be freed due to the
|
||||||
|
@ -60,9 +53,6 @@ The :mod:`gc` module provides the following functions:
|
||||||
The effect of calling ``gc.collect()`` while the interpreter is already
|
The effect of calling ``gc.collect()`` while the interpreter is already
|
||||||
performing a collection is undefined.
|
performing a collection is undefined.
|
||||||
|
|
||||||
.. versionchanged:: 3.13
|
|
||||||
``generation=1`` performs an increment of collection.
|
|
||||||
|
|
||||||
|
|
||||||
.. function:: set_debug(flags)
|
.. function:: set_debug(flags)
|
||||||
|
|
||||||
|
@ -78,20 +68,13 @@ The :mod:`gc` module provides the following functions:
|
||||||
|
|
||||||
.. function:: get_objects(generation=None)
|
.. function:: get_objects(generation=None)
|
||||||
|
|
||||||
|
|
||||||
Returns a list of all objects tracked by the collector, excluding the list
|
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:
|
returned. If *generation* is not ``None``, return only the objects tracked by
|
||||||
|
the collector that are in that generation.
|
||||||
* 0: All objects in the young generation
|
|
||||||
* 1: No objects, as there is no generation 1 (as of Python 3.13)
|
|
||||||
* 2: All objects in the old generation
|
|
||||||
|
|
||||||
.. versionchanged:: 3.8
|
.. versionchanged:: 3.8
|
||||||
New *generation* parameter.
|
New *generation* parameter.
|
||||||
|
|
||||||
.. versionchanged:: 3.13
|
|
||||||
Generation 1 is removed
|
|
||||||
|
|
||||||
.. audit-event:: gc.get_objects generation gc.get_objects
|
.. audit-event:: gc.get_objects generation gc.get_objects
|
||||||
|
|
||||||
.. function:: get_stats()
|
.. function:: get_stats()
|
||||||
|
@ -118,27 +101,19 @@ The :mod:`gc` module provides the following functions:
|
||||||
Set the garbage collection thresholds (the collection frequency). Setting
|
Set the garbage collection thresholds (the collection frequency). Setting
|
||||||
*threshold0* to zero disables collection.
|
*threshold0* to zero disables collection.
|
||||||
|
|
||||||
The GC classifies objects into two generations depending on whether they have
|
The GC classifies objects into three generations depending on how many
|
||||||
survived a collection. New objects are placed in the young generation. If an
|
collection sweeps they have survived. New objects are placed in the youngest
|
||||||
object survives a collection it is moved into the old generation.
|
generation (generation ``0``). If an object survives a collection it is moved
|
||||||
|
into the next older generation. Since generation ``2`` is the oldest
|
||||||
In order to decide when to run, the collector keeps track of the number of object
|
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 and deallocations since the last collection. When the number of
|
||||||
allocations minus the number of deallocations exceeds *threshold0*, collection
|
allocations minus the number of deallocations exceeds *threshold0*, collection
|
||||||
starts. For each collection, all the objects in the young generation and some
|
starts. Initially only generation ``0`` is examined. If generation ``0`` has
|
||||||
fraction of the old generation is collected.
|
been examined more than *threshold1* times since generation ``1`` has been
|
||||||
|
examined, then generation ``1`` is examined as well.
|
||||||
The fraction of the old generation that is collected is **inversely** proportional
|
With the third generation, things are a bit more complicated,
|
||||||
to *threshold1*. The larger *threshold1* is, the slower objects in the old generation
|
see `Collecting the oldest generation <https://devguide.python.org/garbage_collector/#collecting-the-oldest-generation>`_ for more information.
|
||||||
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 <https://devguide.python.org/garbage_collector>`_ for more information.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.13
|
|
||||||
*threshold2* is ignored
|
|
||||||
|
|
||||||
|
|
||||||
.. function:: get_count()
|
.. function:: get_count()
|
||||||
|
|
|
@ -501,30 +501,6 @@ are not tier 3 supported platforms, but will have best-effort support.
|
||||||
.. seealso:: :pep:`730`, :pep:`738`
|
.. seealso:: :pep:`730`, :pep:`738`
|
||||||
|
|
||||||
|
|
||||||
.. _whatsnew313-incremental-gc:
|
|
||||||
|
|
||||||
Incremental garbage collection
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
The cycle garbage collector is now incremental.
|
|
||||||
This means that maximum pause times are reduced
|
|
||||||
by an order of magnitude or more for larger heaps.
|
|
||||||
|
|
||||||
There are now only two generations: young and old.
|
|
||||||
When :func:`gc.collect` is not called directly, the
|
|
||||||
GC is invoked a little less frequently. When invoked, it
|
|
||||||
collects the young generation and an increment of the
|
|
||||||
old generation, instead of collecting one or more generations.
|
|
||||||
|
|
||||||
The behavior of :func:`!gc.collect` changes slightly:
|
|
||||||
|
|
||||||
* ``gc.collect(1)``: Performs an increment of garbage collection,
|
|
||||||
rather than collecting generation 1.
|
|
||||||
* Other calls to :func:`!gc.collect` are unchanged.
|
|
||||||
|
|
||||||
(Contributed by Mark Shannon in :gh:`108362`.)
|
|
||||||
|
|
||||||
|
|
||||||
Other Language Changes
|
Other Language Changes
|
||||||
======================
|
======================
|
||||||
|
|
||||||
|
@ -921,36 +897,6 @@ fractions
|
||||||
(Contributed by Mark Dickinson in :gh:`111320`.)
|
(Contributed by Mark Dickinson in :gh:`111320`.)
|
||||||
|
|
||||||
|
|
||||||
gc
|
|
||||||
--
|
|
||||||
|
|
||||||
The cyclic garbage collector is now incremental,
|
|
||||||
which changes the meaning of the results of
|
|
||||||
:meth:`~gc.get_threshold` and :meth:`~gc.set_threshold`
|
|
||||||
as well as :meth:`~gc.get_count` and :meth:`~gc.get_stats`.
|
|
||||||
|
|
||||||
* For backwards compatibility, :meth:`~gc.get_threshold` continues to return
|
|
||||||
a three-item tuple.
|
|
||||||
The first value is the threshold for young collections, as before;
|
|
||||||
the second value determines the rate at which the old collection is scanned
|
|
||||||
(the default is 10, and higher values mean that the old collection
|
|
||||||
is scanned more slowly).
|
|
||||||
The third value is meaningless and is always zero.
|
|
||||||
|
|
||||||
* :meth:`~gc.set_threshold` ignores any items after the second.
|
|
||||||
|
|
||||||
* :meth:`~gc.get_count` and :meth:`~gc.get_stats` continue to return
|
|
||||||
the same format of results.
|
|
||||||
The only difference is that instead of the results referring to
|
|
||||||
the young, aging and old generations,
|
|
||||||
the results refer to the young generation
|
|
||||||
and the aging and collecting spaces of the old generation.
|
|
||||||
|
|
||||||
In summary, code that attempted to manipulate the behavior of the cycle GC
|
|
||||||
may not work exactly as intended, but it is very unlikely to be harmful.
|
|
||||||
All other code will work just fine.
|
|
||||||
|
|
||||||
|
|
||||||
glob
|
glob
|
||||||
----
|
----
|
||||||
|
|
||||||
|
@ -1515,11 +1461,6 @@ zipimport
|
||||||
Optimizations
|
Optimizations
|
||||||
=============
|
=============
|
||||||
|
|
||||||
* The new :ref:`incremental garbage collector <whatsnew313-incremental-gc>`
|
|
||||||
means that maximum pause times are reduced
|
|
||||||
by an order of magnitude or more for larger heaps.
|
|
||||||
(Contributed by Mark Shannon in :gh:`108362`.)
|
|
||||||
|
|
||||||
* Several standard library modules have had
|
* Several standard library modules have had
|
||||||
their import times significantly improved.
|
their import times significantly improved.
|
||||||
For example, the import time of the :mod:`typing` module
|
For example, the import time of the :mod:`typing` module
|
||||||
|
@ -2632,13 +2573,6 @@ Changes in the Python API
|
||||||
Wrap it in :func:`staticmethod` if you want to preserve the old behavior.
|
Wrap it in :func:`staticmethod` if you want to preserve the old behavior.
|
||||||
(Contributed by Serhiy Storchaka in :gh:`121027`.)
|
(Contributed by Serhiy Storchaka in :gh:`121027`.)
|
||||||
|
|
||||||
* The :ref:`garbage collector is now incremental <whatsnew313-incremental-gc>`,
|
|
||||||
which means that the behavior of :func:`gc.collect` changes slightly:
|
|
||||||
|
|
||||||
* ``gc.collect(1)``: Performs an increment of garbage collection,
|
|
||||||
rather than collecting generation 1.
|
|
||||||
* Other calls to :func:`!gc.collect` are unchanged.
|
|
||||||
|
|
||||||
* An :exc:`OSError` is now raised by :func:`getpass.getuser`
|
* An :exc:`OSError` is now raised by :func:`getpass.getuser`
|
||||||
for any failure to retrieve a username,
|
for any failure to retrieve a username,
|
||||||
instead of :exc:`ImportError` on non-Unix platforms
|
instead of :exc:`ImportError` on non-Unix platforms
|
||||||
|
|
|
@ -142,26 +142,11 @@ static inline void _PyObject_GC_SET_SHARED_INLINE(PyObject *op) {
|
||||||
|
|
||||||
/* Bit flags for _gc_prev */
|
/* Bit flags for _gc_prev */
|
||||||
/* Bit 0 is set when tp_finalize is called */
|
/* Bit 0 is set when tp_finalize is called */
|
||||||
#define _PyGC_PREV_MASK_FINALIZED 1
|
#define _PyGC_PREV_MASK_FINALIZED (1)
|
||||||
/* Bit 1 is set when the object is in generation which is GCed currently. */
|
/* Bit 1 is set when the object is in generation which is GCed currently. */
|
||||||
#define _PyGC_PREV_MASK_COLLECTING 2
|
#define _PyGC_PREV_MASK_COLLECTING (2)
|
||||||
|
/* The (N-2) most significant bits contain the real address. */
|
||||||
/* Bit 0 in _gc_next is the old space bit.
|
#define _PyGC_PREV_SHIFT (2)
|
||||||
* It is set as follows:
|
|
||||||
* Young: gcstate->visited_space
|
|
||||||
* old[0]: 0
|
|
||||||
* old[1]: 1
|
|
||||||
* permanent: 0
|
|
||||||
*
|
|
||||||
* During a collection all objects handled should have the bit set to
|
|
||||||
* gcstate->visited_space, as objects are moved from the young gen
|
|
||||||
* and the increment into old[gcstate->visited_space].
|
|
||||||
* 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)
|
#define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT)
|
||||||
|
|
||||||
/* set for debugging information */
|
/* set for debugging information */
|
||||||
|
@ -187,13 +172,11 @@ typedef enum {
|
||||||
// Lowest bit of _gc_next is used for flags only in GC.
|
// Lowest bit of _gc_next is used for flags only in GC.
|
||||||
// But it is always 0 for normal code.
|
// But it is always 0 for normal code.
|
||||||
static inline PyGC_Head* _PyGCHead_NEXT(PyGC_Head *gc) {
|
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;
|
return (PyGC_Head*)next;
|
||||||
}
|
}
|
||||||
static inline void _PyGCHead_SET_NEXT(PyGC_Head *gc, PyGC_Head *next) {
|
static inline void _PyGCHead_SET_NEXT(PyGC_Head *gc, PyGC_Head *next) {
|
||||||
uintptr_t unext = (uintptr_t)next;
|
gc->_gc_next = (uintptr_t)next;
|
||||||
assert((unext & ~_PyGC_PREV_MASK) == 0);
|
|
||||||
gc->_gc_next = (gc->_gc_next & ~_PyGC_PREV_MASK) | unext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags.
|
// Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags.
|
||||||
|
@ -201,7 +184,6 @@ static inline PyGC_Head* _PyGCHead_PREV(PyGC_Head *gc) {
|
||||||
uintptr_t prev = (gc->_gc_prev & _PyGC_PREV_MASK);
|
uintptr_t prev = (gc->_gc_prev & _PyGC_PREV_MASK);
|
||||||
return (PyGC_Head*)prev;
|
return (PyGC_Head*)prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void _PyGCHead_SET_PREV(PyGC_Head *gc, PyGC_Head *prev) {
|
static inline void _PyGCHead_SET_PREV(PyGC_Head *gc, PyGC_Head *prev) {
|
||||||
uintptr_t uprev = (uintptr_t)prev;
|
uintptr_t uprev = (uintptr_t)prev;
|
||||||
assert((uprev & ~_PyGC_PREV_MASK) == 0);
|
assert((uprev & ~_PyGC_PREV_MASK) == 0);
|
||||||
|
@ -287,13 +269,6 @@ struct gc_generation {
|
||||||
generations */
|
generations */
|
||||||
};
|
};
|
||||||
|
|
||||||
struct gc_collection_stats {
|
|
||||||
/* number of collected objects */
|
|
||||||
Py_ssize_t collected;
|
|
||||||
/* total number of uncollectable objects (put into gc.garbage) */
|
|
||||||
Py_ssize_t uncollectable;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Running stats per generation */
|
/* Running stats per generation */
|
||||||
struct gc_generation_stats {
|
struct gc_generation_stats {
|
||||||
/* total number of collections */
|
/* total number of collections */
|
||||||
|
@ -315,8 +290,8 @@ struct _gc_runtime_state {
|
||||||
int enabled;
|
int enabled;
|
||||||
int debug;
|
int debug;
|
||||||
/* linked lists of container objects */
|
/* linked lists of container objects */
|
||||||
struct gc_generation young;
|
struct gc_generation generations[NUM_GENERATIONS];
|
||||||
struct gc_generation old[2];
|
PyGC_Head *generation0;
|
||||||
/* a permanent generation which won't be collected */
|
/* a permanent generation which won't be collected */
|
||||||
struct gc_generation permanent_generation;
|
struct gc_generation permanent_generation;
|
||||||
struct gc_generation_stats generation_stats[NUM_GENERATIONS];
|
struct gc_generation_stats generation_stats[NUM_GENERATIONS];
|
||||||
|
@ -327,12 +302,6 @@ struct _gc_runtime_state {
|
||||||
/* a list of callbacks to be invoked when collection is performed */
|
/* a list of callbacks to be invoked when collection is performed */
|
||||||
PyObject *callbacks;
|
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;
|
|
||||||
|
|
||||||
#ifdef Py_GIL_DISABLED
|
|
||||||
/* This is the number of objects that survived the last full
|
/* This is the number of objects that survived the last full
|
||||||
collection. It approximates the number of long lived objects
|
collection. It approximates the number of long lived objects
|
||||||
tracked by the GC.
|
tracked by the GC.
|
||||||
|
@ -345,6 +314,7 @@ struct _gc_runtime_state {
|
||||||
the first time. */
|
the first time. */
|
||||||
Py_ssize_t long_lived_pending;
|
Py_ssize_t long_lived_pending;
|
||||||
|
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
/* gh-117783: Deferred reference counting is not fully implemented yet, so
|
/* gh-117783: Deferred reference counting is not fully implemented yet, so
|
||||||
as a temporary measure we treat objects using deferred reference
|
as a temporary measure we treat objects using deferred reference
|
||||||
counting as immortal. The value may be zero, one, or a negative number:
|
counting as immortal. The value may be zero, one, or a negative number:
|
||||||
|
@ -365,7 +335,8 @@ struct _gc_thread_state {
|
||||||
|
|
||||||
extern void _PyGC_InitState(struct _gc_runtime_state *);
|
extern void _PyGC_InitState(struct _gc_runtime_state *);
|
||||||
|
|
||||||
extern Py_ssize_t _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason);
|
extern Py_ssize_t _PyGC_Collect(PyThreadState *tstate, int generation,
|
||||||
|
_PyGC_Reason reason);
|
||||||
extern void _PyGC_CollectNoFail(PyThreadState *tstate);
|
extern void _PyGC_CollectNoFail(PyThreadState *tstate);
|
||||||
|
|
||||||
/* Freeze objects tracked by the GC and ignore them in future collections. */
|
/* Freeze objects tracked by the GC and ignore them in future collections. */
|
||||||
|
|
|
@ -353,12 +353,11 @@ static inline void _PyObject_GC_TRACK(
|
||||||
filename, lineno, __func__);
|
filename, lineno, __func__);
|
||||||
|
|
||||||
PyInterpreterState *interp = _PyInterpreterState_GET();
|
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||||||
PyGC_Head *generation0 = &interp->gc.young.head;
|
PyGC_Head *generation0 = interp->gc.generation0;
|
||||||
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
|
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
|
||||||
_PyGCHead_SET_NEXT(last, gc);
|
_PyGCHead_SET_NEXT(last, gc);
|
||||||
_PyGCHead_SET_PREV(gc, last);
|
_PyGCHead_SET_PREV(gc, last);
|
||||||
/* Young objects will be moved into the visited space during GC, so set the bit here */
|
_PyGCHead_SET_NEXT(gc, generation0);
|
||||||
gc->_gc_next = ((uintptr_t)generation0) | interp->gc.visited_space;
|
|
||||||
generation0->_gc_prev = (uintptr_t)gc;
|
generation0->_gc_prev = (uintptr_t)gc;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,12 +228,12 @@ extern PyTypeObject _PyExc_MemoryError;
|
||||||
}, \
|
}, \
|
||||||
.gc = { \
|
.gc = { \
|
||||||
.enabled = 1, \
|
.enabled = 1, \
|
||||||
.young = { .threshold = 2000, }, \
|
.generations = { \
|
||||||
.old = { \
|
/* .head is set in _PyGC_InitState(). */ \
|
||||||
|
{ .threshold = 2000, }, \
|
||||||
|
{ .threshold = 10, }, \
|
||||||
{ .threshold = 10, }, \
|
{ .threshold = 10, }, \
|
||||||
{ .threshold = 0, }, \
|
|
||||||
}, \
|
}, \
|
||||||
.work_to_do = -5000, \
|
|
||||||
}, \
|
}, \
|
||||||
.qsbr = { \
|
.qsbr = { \
|
||||||
.wr_seq = QSBR_INITIAL, \
|
.wr_seq = QSBR_INITIAL, \
|
||||||
|
|
|
@ -392,11 +392,19 @@ class GCTests(unittest.TestCase):
|
||||||
# each call to collect(N)
|
# each call to collect(N)
|
||||||
x = []
|
x = []
|
||||||
gc.collect(0)
|
gc.collect(0)
|
||||||
# x is now in the old gen
|
# x is now in gen 1
|
||||||
a, b, c = gc.get_count()
|
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.
|
# internal implementation details of the interpreter.
|
||||||
self.assertEqual((b, c), (1, 0))
|
self.assertEqual((b, c), (1, 0))
|
||||||
|
self.assertEqual((e, f), (0, 1))
|
||||||
|
self.assertEqual((h, i), (0, 0))
|
||||||
|
|
||||||
def test_trashcan(self):
|
def test_trashcan(self):
|
||||||
class Ouch:
|
class Ouch:
|
||||||
|
@ -835,10 +843,42 @@ class GCTests(unittest.TestCase):
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any(l is element for element in gc.get_objects(generation=0))
|
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(
|
self.assertFalse(
|
||||||
any(l is element for element in gc.get_objects(generation=0))
|
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
|
del l
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
@ -1066,72 +1106,6 @@ class GCTests(unittest.TestCase):
|
||||||
gc.get_referents(tracked_capsule)
|
gc.get_referents(tracked_capsule)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IncrementalGCTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# Reenable GC as it is disabled module-wide
|
|
||||||
gc.enable()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
gc.disable()
|
|
||||||
|
|
||||||
@requires_gil_enabled("Free threading does not support incremental GC")
|
|
||||||
# Use small increments to emulate longer running process in a shorter time
|
|
||||||
@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)
|
|
||||||
count = 1000
|
|
||||||
|
|
||||||
# There will be some objects we aren't counting,
|
|
||||||
# e.g. the gc stats dicts. This test checks
|
|
||||||
# that the counts don't grow, so we try to
|
|
||||||
# correct for the uncounted objects
|
|
||||||
# This is just an estimate.
|
|
||||||
CORRECTION = 20
|
|
||||||
|
|
||||||
enabled = gc.isenabled()
|
|
||||||
gc.enable()
|
|
||||||
olds = []
|
|
||||||
for i in range(20_000):
|
|
||||||
newhead = make_ll(20)
|
|
||||||
count += 20
|
|
||||||
newhead.surprise = head
|
|
||||||
olds.append(newhead)
|
|
||||||
if len(olds) == 20:
|
|
||||||
stats = gc.get_stats()
|
|
||||||
young = stats[0]
|
|
||||||
incremental = stats[1]
|
|
||||||
old = stats[2]
|
|
||||||
collected = young['collected'] + incremental['collected'] + old['collected']
|
|
||||||
count += CORRECTION
|
|
||||||
live = count - collected
|
|
||||||
self.assertLess(live, 25000)
|
|
||||||
del olds[:]
|
|
||||||
if not enabled:
|
|
||||||
gc.disable()
|
|
||||||
|
|
||||||
|
|
||||||
class GCCallbackTests(unittest.TestCase):
|
class GCCallbackTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Save gc state and disable it.
|
# Save gc state and disable it.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Revert the incremental GC (in 3.13), since it's not clear the benefits outweigh the costs at this point.
|
|
@ -158,12 +158,17 @@ gc_set_threshold_impl(PyObject *module, int threshold0, int group_right_1,
|
||||||
{
|
{
|
||||||
GCState *gcstate = get_gc_state();
|
GCState *gcstate = get_gc_state();
|
||||||
|
|
||||||
gcstate->young.threshold = threshold0;
|
gcstate->generations[0].threshold = threshold0;
|
||||||
if (group_right_1) {
|
if (group_right_1) {
|
||||||
gcstate->old[0].threshold = threshold1;
|
gcstate->generations[1].threshold = threshold1;
|
||||||
}
|
}
|
||||||
if (group_right_2) {
|
if (group_right_2) {
|
||||||
gcstate->old[1].threshold = threshold2;
|
gcstate->generations[2].threshold = threshold2;
|
||||||
|
|
||||||
|
/* generations higher than 2 get the same threshold */
|
||||||
|
for (int i = 3; i < NUM_GENERATIONS; i++) {
|
||||||
|
gcstate->generations[i].threshold = gcstate->generations[2].threshold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
@ -180,9 +185,9 @@ gc_get_threshold_impl(PyObject *module)
|
||||||
{
|
{
|
||||||
GCState *gcstate = get_gc_state();
|
GCState *gcstate = get_gc_state();
|
||||||
return Py_BuildValue("(iii)",
|
return Py_BuildValue("(iii)",
|
||||||
gcstate->young.threshold,
|
gcstate->generations[0].threshold,
|
||||||
gcstate->old[0].threshold,
|
gcstate->generations[1].threshold,
|
||||||
0);
|
gcstate->generations[2].threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
|
@ -202,14 +207,14 @@ gc_get_count_impl(PyObject *module)
|
||||||
struct _gc_thread_state *gc = &tstate->gc;
|
struct _gc_thread_state *gc = &tstate->gc;
|
||||||
|
|
||||||
// Flush the local allocation count to the global count
|
// Flush the local allocation count to the global count
|
||||||
_Py_atomic_add_int(&gcstate->young.count, (int)gc->alloc_count);
|
_Py_atomic_add_int(&gcstate->generations[0].count, (int)gc->alloc_count);
|
||||||
gc->alloc_count = 0;
|
gc->alloc_count = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return Py_BuildValue("(iii)",
|
return Py_BuildValue("(iii)",
|
||||||
gcstate->young.count,
|
gcstate->generations[0].count,
|
||||||
gcstate->old[gcstate->visited_space].count,
|
gcstate->generations[1].count,
|
||||||
gcstate->old[gcstate->visited_space^1].count);
|
gcstate->generations[2].count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
|
|
994
Python/gc.c
994
Python/gc.c
File diff suppressed because it is too large
Load diff
|
@ -744,7 +744,7 @@ void
|
||||||
_PyGC_InitState(GCState *gcstate)
|
_PyGC_InitState(GCState *gcstate)
|
||||||
{
|
{
|
||||||
// TODO: move to pycore_runtime_init.h once the incremental GC lands.
|
// TODO: move to pycore_runtime_init.h once the incremental GC lands.
|
||||||
gcstate->young.threshold = 2000;
|
gcstate->generations[0].threshold = 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1042,8 +1042,8 @@ cleanup_worklist(struct worklist *worklist)
|
||||||
static bool
|
static bool
|
||||||
gc_should_collect(GCState *gcstate)
|
gc_should_collect(GCState *gcstate)
|
||||||
{
|
{
|
||||||
int count = _Py_atomic_load_int_relaxed(&gcstate->young.count);
|
int count = _Py_atomic_load_int_relaxed(&gcstate->generations[0].count);
|
||||||
int threshold = gcstate->young.threshold;
|
int threshold = gcstate->generations[0].threshold;
|
||||||
if (count <= threshold || threshold == 0 || !gcstate->enabled) {
|
if (count <= threshold || threshold == 0 || !gcstate->enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1051,7 +1051,7 @@ gc_should_collect(GCState *gcstate)
|
||||||
// objects. A few tests rely on immediate scheduling of the GC so we ignore
|
// objects. A few tests rely on immediate scheduling of the GC so we ignore
|
||||||
// the scaled threshold if generations[1].threshold is set to zero.
|
// the scaled threshold if generations[1].threshold is set to zero.
|
||||||
return (count > gcstate->long_lived_total / 4 ||
|
return (count > gcstate->long_lived_total / 4 ||
|
||||||
gcstate->old[0].threshold == 0);
|
gcstate->generations[1].threshold == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -1065,7 +1065,7 @@ record_allocation(PyThreadState *tstate)
|
||||||
if (gc->alloc_count >= LOCAL_ALLOC_COUNT_THRESHOLD) {
|
if (gc->alloc_count >= LOCAL_ALLOC_COUNT_THRESHOLD) {
|
||||||
// TODO: Use Py_ssize_t for the generation count.
|
// TODO: Use Py_ssize_t for the generation count.
|
||||||
GCState *gcstate = &tstate->interp->gc;
|
GCState *gcstate = &tstate->interp->gc;
|
||||||
_Py_atomic_add_int(&gcstate->young.count, (int)gc->alloc_count);
|
_Py_atomic_add_int(&gcstate->generations[0].count, (int)gc->alloc_count);
|
||||||
gc->alloc_count = 0;
|
gc->alloc_count = 0;
|
||||||
|
|
||||||
if (gc_should_collect(gcstate) &&
|
if (gc_should_collect(gcstate) &&
|
||||||
|
@ -1084,7 +1084,7 @@ record_deallocation(PyThreadState *tstate)
|
||||||
gc->alloc_count--;
|
gc->alloc_count--;
|
||||||
if (gc->alloc_count <= -LOCAL_ALLOC_COUNT_THRESHOLD) {
|
if (gc->alloc_count <= -LOCAL_ALLOC_COUNT_THRESHOLD) {
|
||||||
GCState *gcstate = &tstate->interp->gc;
|
GCState *gcstate = &tstate->interp->gc;
|
||||||
_Py_atomic_add_int(&gcstate->young.count, (int)gc->alloc_count);
|
_Py_atomic_add_int(&gcstate->generations[0].count, (int)gc->alloc_count);
|
||||||
gc->alloc_count = 0;
|
gc->alloc_count = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1096,12 +1096,10 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
|
||||||
|
|
||||||
// update collection and allocation counters
|
// update collection and allocation counters
|
||||||
if (generation+1 < NUM_GENERATIONS) {
|
if (generation+1 < NUM_GENERATIONS) {
|
||||||
state->gcstate->old[generation].count += 1;
|
state->gcstate->generations[generation+1].count += 1;
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i <= generation; i++) {
|
||||||
state->gcstate->young.count = 0;
|
state->gcstate->generations[i].count = 0;
|
||||||
for (int i = 1; i <= generation; ++i) {
|
|
||||||
state->gcstate->old[i-1].count = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge refcounts for all queued objects
|
// merge refcounts for all queued objects
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue