Skip to content

Commit e878e1e

Browse files
authored
gh-159137: Untrack tuples and count survivors in move_unreachable() (#159154)
1 parent 77f7b96 commit e878e1e

2 files changed

Lines changed: 31 additions & 35 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Speed up the garbage collector by untracking tuples and counting the surviving
2+
objects while it looks for unreachable objects, which saves two walks of the
3+
collected generation in every collection.

‎Python/gc.c‎

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -576,9 +576,10 @@ visit_reachable(PyObject *op, void *arg)
576576
* But _gc_next in unreachable list has NEXT_MASK_UNREACHABLE flag.
577577
* So we can not gc_list_* functions for unreachable until we remove the flag.
578578
*/
579-
static void
579+
static Py_ssize_t
580580
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
581581
{
582+
Py_ssize_t reachable = 0;
582583
// previous elem in the young list, used for restore gc_prev.
583584
PyGC_Head *prev = young;
584585
PyGC_Head *gc = GC_NEXT(young);
@@ -615,7 +616,21 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
615616
_PyGCHead_SET_PREV(gc, prev);
616617
// gc is not COLLECTING state after here.
617618
gc_clear_collecting(gc);
618-
prev = gc;
619+
if (PyTuple_CheckExact(op)) {
620+
// Untracking gc overwrites the gc_refs of the next object.
621+
PyGC_Head *next = GC_NEXT(gc);
622+
uintptr_t next_refs = next->_gc_prev;
623+
_PyTuple_MaybeUntrack(op);
624+
next->_gc_prev = next_refs;
625+
if (gc->_gc_next != 0) {
626+
prev = gc;
627+
reachable++;
628+
}
629+
}
630+
else {
631+
prev = gc;
632+
reachable++;
633+
}
619634
}
620635
else {
621636
/* This *may* be unreachable. To make progress,
@@ -648,32 +663,7 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
648663
young->_gc_prev = (uintptr_t)prev;
649664
// don't let the pollution of the list head's next pointer leak
650665
unreachable->_gc_next &= ~NEXT_MASK_UNREACHABLE;
651-
}
652-
653-
/* In theory, all tuples should be younger than the
654-
* objects they refer to, as tuples are immortal.
655-
* Therefore, untracking tuples in oldest-first order in the
656-
* young generation before promoting them should have tracked
657-
* all the tuples that can be untracked.
658-
*
659-
* Unfortunately, the C API allows tuples to be created
660-
* and then filled in. So this won't untrack all tuples
661-
* that can be untracked. It should untrack most of them
662-
* and is much faster than a more complex approach that
663-
* would untrack all relevant tuples.
664-
*/
665-
static void
666-
untrack_tuples(PyGC_Head *head)
667-
{
668-
PyGC_Head *gc = GC_NEXT(head);
669-
while (gc != head) {
670-
PyObject *op = FROM_GC(gc);
671-
PyGC_Head *next = GC_NEXT(gc);
672-
if (PyTuple_CheckExact(op)) {
673-
_PyTuple_MaybeUntrack(op);
674-
}
675-
gc = next;
676-
}
666+
return reachable;
677667
}
678668

679669
/* Return true if object has a pre-PEP 442 finalization method. */
@@ -1175,7 +1165,8 @@ flag is cleared (for example, by using 'clear_unreachable_mask' function or
11751165
by a call to 'move_legacy_finalizers'), the 'unreachable' list is not a normal
11761166
list and we can not use most gc_list_* functions for it. */
11771167
static inline Py_ssize_t
1178-
deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
1168+
deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable,
1169+
Py_ssize_t *reachable) {
11791170
validate_list(base, collecting_clear_unreachable_clear);
11801171
/* Using ob_refcnt and gc_refs, calculate which objects in the
11811172
* container set are reachable from outside the set (i.e., have a
@@ -1221,7 +1212,8 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
12211212
* worth complicating the code to speed just a little.
12221213
*/
12231214
gc_list_init(unreachable);
1224-
move_unreachable(base, unreachable); // gc_prev is pointer again
1215+
// gc_prev is pointer again
1216+
*reachable = move_unreachable(base, unreachable);
12251217
validate_list(base, collecting_clear_unreachable_clear);
12261218
validate_list(unreachable, collecting_set_unreachable_set);
12271219
return candidates;
@@ -1252,7 +1244,8 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable,
12521244
// have the PREV_MARK_COLLECTING set, but the objects are going to be
12531245
// removed so we can skip the expense of clearing the flag.
12541246
PyGC_Head* resurrected = unreachable;
1255-
deduce_unreachable(resurrected, still_unreachable);
1247+
Py_ssize_t reachable;
1248+
deduce_unreachable(resurrected, still_unreachable, &reachable);
12561249
clear_unreachable_mask(still_unreachable);
12571250

12581251
// Move the resurrected objects to the old generation for future collection.
@@ -1521,13 +1514,13 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
15211514
}
15221515
validate_list(old, collecting_clear_unreachable_clear);
15231516

1524-
stats.candidates = deduce_unreachable(young, &unreachable);
1517+
Py_ssize_t reachable;
1518+
stats.candidates = deduce_unreachable(young, &unreachable, &reachable);
15251519

1526-
untrack_tuples(young);
15271520
/* Move reachable objects to next generation. */
15281521
if (young != old) {
15291522
if (generation == NUM_GENERATIONS - 2) {
1530-
gcstate->long_lived_pending += gc_list_size(young);
1523+
gcstate->long_lived_pending += reachable;
15311524
}
15321525
gc_list_merge(young, old);
15331526
}
@@ -1540,7 +1533,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
15401533
// site; the cost is slightly more work for full collections on dicts
15411534
// with only atomic values.
15421535
gcstate->long_lived_pending = 0;
1543-
gcstate->long_lived_total = gc_list_size(young);
1536+
gcstate->long_lived_total = reachable;
15441537
}
15451538

15461539
/* All objects in unreachable are trash, but objects reachable from

0 commit comments

Comments
 (0)