Skip to content

Commit ac3ad26

Browse files
committed
Partially apply mark-first-gc patch
1 parent 754bb68 commit ac3ad26

File tree

7 files changed

+73
-12
lines changed

7 files changed

+73
-12
lines changed

Include/cpython/pystats.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ typedef struct _gc_stats {
9999
uint64_t collections;
100100
uint64_t object_visits;
101101
uint64_t objects_collected;
102+
uint64_t objects_transitively_reachable;
103+
uint64_t objects_not_transitively_reachable;
102104
} GCStats;
103105

104106
typedef struct _uop_stats {

Include/internal/pycore_frame.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ typedef struct _PyInterpreterFrame {
7575
_PyStackRef *stackpointer;
7676
uint16_t return_offset; /* Only relevant during a function call */
7777
char owner;
78+
char visited;
7879
/* Locals and stack */
7980
_PyStackRef localsplus[1];
8081
} _PyInterpreterFrame;
@@ -207,6 +208,7 @@ _PyFrame_Initialize(
207208
#endif
208209
frame->return_offset = 0;
209210
frame->owner = FRAME_OWNED_BY_THREAD;
211+
frame->visited = 0;
210212

211213
for (int i = null_locals_from; i < code->co_nlocalsplus; i++) {
212214
frame->localsplus[i] = PyStackRef_NULL;
@@ -389,6 +391,7 @@ _PyFrame_PushTrampolineUnchecked(PyThreadState *tstate, PyCodeObject *code, int
389391
frame->instr_ptr = _PyCode_CODE(code);
390392
#endif
391393
frame->owner = FRAME_OWNED_BY_THREAD;
394+
frame->visited = 0;
392395
frame->return_offset = 0;
393396

394397
#ifdef Py_GIL_DISABLED

Include/internal/pycore_gc.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ extern "C" {
1010

1111
/* GC information is stored BEFORE the object structure. */
1212
typedef struct {
13-
// Pointer to next object in the list.
13+
// Tagged pointer to next object in the list.
1414
// 0 means the object is not tracked
1515
uintptr_t _gc_next;
1616

17-
// Pointer to previous object in the list.
17+
// Tagged pointer to previous object in the list.
1818
// Lowest two bits are used for flags documented later.
1919
uintptr_t _gc_prev;
2020
} PyGC_Head;
@@ -302,6 +302,11 @@ struct gc_generation_stats {
302302
Py_ssize_t uncollectable;
303303
};
304304

305+
enum _GCPhase {
306+
GC_PHASE_MARK = 0,
307+
GC_PHASE_COLLECT = 1
308+
};
309+
305310
struct _gc_runtime_state {
306311
/* List of objects that still need to be cleaned up, singly linked
307312
* via their gc headers' gc_prev pointers. */
@@ -329,6 +334,7 @@ struct _gc_runtime_state {
329334
Py_ssize_t work_to_do;
330335
/* Which of the old spaces is the visited space */
331336
int visited_space;
337+
int phase;
332338

333339
#ifdef Py_GIL_DISABLED
334340
/* This is the number of objects that survived the last full

Include/internal/pycore_runtime_init.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ extern PyTypeObject _PyExc_MemoryError;
137137
{ .threshold = 0, }, \
138138
}, \
139139
.work_to_do = -5000, \
140+
.phase = GC_PHASE_MARK, \
140141
}, \
141142
.qsbr = { \
142143
.wr_seq = QSBR_INITIAL, \

InternalDocs/garbage_collector.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,45 @@ specifically in a generation by calling `gc.collect(generation=NUM)`.
484484
```
485485

486486

487+
Optimization: visiting reachable objects
488+
========================================
489+
490+
An object cannot be garbage if it can be reached.
491+
492+
To avoid having to identify reference cycles across the whole heap, we can
493+
reduce the amount of work done considerably by first moving most reachable objects
494+
to the `visited` space. Empirically, most reachable objects can be reached from a
495+
small set of global objects and local variables.
496+
This step does much less work per object, so reduces the time spent
497+
performing garbage collection by at least half.
498+
499+
> [!NOTE]
500+
> Objects that are not determined to be reachable by this pass are not necessarily
501+
> unreachable. We still need to perform the main algorithm to determine which objects
502+
> are actually unreachable.
503+
We use the same technique of forming a transitive closure as the incremental
504+
collector does to find reachable objects, seeding the list with some global
505+
objects and the currently executing frames.
506+
507+
This phase moves objects to the `visited` space, as follows:
508+
509+
1. All objects directly referred to by any builtin class, the `sys` module, the `builtins`
510+
module and all objects directly referred to from stack frames are added to a working
511+
set of reachable objects.
512+
2. Until this working set is empty:
513+
1. Pop an object from the set and move it to the `visited` space
514+
2. For each object directly reachable from that object:
515+
* If it is not already in `visited` space and it is a GC object,
516+
add it to the working set
517+
518+
519+
Before each increment of collection is performed, the stacks are scanned
520+
to check for any new stack frames that have been created since the last
521+
increment. All objects directly referred to from those stack frames are
522+
added to the working set.
523+
Then the above algorithm is repeated, starting from step 2.
524+
525+
487526
Optimization: reusing fields to save memory
488527
===========================================
489528

Lib/test/test_gc.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def __new__(cls, *args, **kwargs):
3131
return C
3232
ContainerNoGC = None
3333

34+
try:
35+
import _testinternalcapi
36+
except ImportError:
37+
_testinternalcapi = None
38+
3439
### Support code
3540
###############################################################################
3641

@@ -1130,6 +1135,7 @@ def setUp(self):
11301135
def tearDown(self):
11311136
gc.disable()
11321137

1138+
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
11331139
@requires_gil_enabled("Free threading does not support incremental GC")
11341140
# Use small increments to emulate longer running process in a shorter time
11351141
@gc_threshold(200, 10)
@@ -1167,20 +1173,15 @@ def make_ll(depth):
11671173
enabled = gc.isenabled()
11681174
gc.enable()
11691175
olds = []
1176+
initial_heap_size = _testinternalcapi.get_heap_size()
11701177
for i in range(20_000):
11711178
newhead = make_ll(20)
11721179
count += 20
11731180
newhead.surprise = head
11741181
olds.append(newhead)
11751182
if len(olds) == 20:
1176-
stats = gc.get_stats()
1177-
young = stats[0]
1178-
incremental = stats[1]
1179-
old = stats[2]
1180-
collected = young['collected'] + incremental['collected'] + old['collected']
1181-
count += CORRECTION
1182-
live = count - collected
1183-
self.assertLess(live, 25000)
1183+
new_objects = _testinternalcapi.get_heap_size() - initial_heap_size
1184+
self.assertLess(new_objects, 25_000)
11841185
del olds[:]
11851186
if not enabled:
11861187
gc.disable()
@@ -1322,7 +1323,8 @@ def test_refcount_errors(self):
13221323
from test.support import gc_collect, SuppressCrashReport
13231324
13241325
a = [1, 2, 3]
1325-
b = [a]
1326+
b = [a, a]
1327+
a.append(b)
13261328
13271329
# Avoid coredump when Py_FatalError() calls abort()
13281330
SuppressCrashReport().__enter__()
@@ -1332,6 +1334,8 @@ def test_refcount_errors(self):
13321334
# (to avoid deallocating it):
13331335
import ctypes
13341336
ctypes.pythonapi.Py_DecRef(ctypes.py_object(a))
1337+
del a
1338+
del b
13351339
13361340
# The garbage collector should now have a fatal error
13371341
# when it reaches the broken object
@@ -1360,7 +1364,7 @@ def test_refcount_errors(self):
13601364
self.assertRegex(stderr,
13611365
br'object type name: list')
13621366
self.assertRegex(stderr,
1363-
br'object repr : \[1, 2, 3\]')
1367+
br'object repr : \[1, 2, 3, \[\[...\], \[...\]\]\]')
13641368

13651369

13661370
class GCTogglingTests(unittest.TestCase):

Modules/_testinternalcapi.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,11 @@ has_deferred_refcount(PyObject *self, PyObject *op)
20762076
return PyBool_FromLong(_PyObject_HasDeferredRefcount(op));
20772077
}
20782078

2079+
static PyObject *
2080+
get_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
2081+
{
2082+
return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size);
2083+
}
20792084

20802085
static PyMethodDef module_functions[] = {
20812086
{"get_configs", get_configs, METH_NOARGS},
@@ -2174,6 +2179,7 @@ static PyMethodDef module_functions[] = {
21742179
{"get_static_builtin_types", get_static_builtin_types, METH_NOARGS},
21752180
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
21762181
{"has_deferred_refcount", has_deferred_refcount, METH_O},
2182+
{"get_heap_size", get_heap_size, METH_NOARGS},
21772183
{NULL, NULL} /* sentinel */
21782184
};
21792185

0 commit comments

Comments
 (0)