Skip to content

Commit dd4384d

Browse files
committed
gh-129210: Avoid list traversal if skipping resurrection checks
If we don't need to repeat the unreachable check we can track the number of objects being collected as a side-effect of previous operations and avoid doing a list traversal to count them. In my environment and testing this gives an additional ~5% performance improvement on top of the previous change for the gh-129210 micro-benchmark.
1 parent d34a745 commit dd4384d

File tree

1 file changed

+14
-4
lines changed

1 file changed

+14
-4
lines changed

Python/gc.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -775,10 +775,13 @@ has_legacy_finalizer(PyObject *op)
775775
*
776776
* This function also removes NEXT_MASK_UNREACHABLE flag
777777
* from _gc_next in unreachable.
778+
*
779+
* Returns the number of items remaining in the `unreachable` list.
778780
*/
779-
static void
781+
static Py_ssize_t
780782
move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
781783
{
784+
Py_ssize_t count = 0;
782785
PyGC_Head *gc, *next;
783786
_PyObject_ASSERT(
784787
FROM_GC(unreachable),
@@ -797,8 +800,11 @@ move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
797800
if (has_legacy_finalizer(op)) {
798801
gc_clear_collecting(gc);
799802
gc_list_move(gc, finalizers);
803+
} else {
804+
++count;
800805
}
801806
}
807+
return count;
802808
}
803809

804810
static inline void
@@ -1072,7 +1078,7 @@ handle_legacy_finalizers(PyThreadState *tstate,
10721078

10731079
/* Run first-time finalizers (if any) on all the objects in collectable.
10741080
* Note that this may remove some (or even all) of the objects from the
1075-
* list, due to refcounts falling to 0.
1081+
* list, due to refcounts falling to 0. Return the number of finalizers run.
10761082
*/
10771083
static int
10781084
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
@@ -1717,7 +1723,8 @@ gc_collect_region(PyThreadState *tstate,
17171723
gc_list_init(&finalizers);
17181724
// NEXT_MASK_UNREACHABLE is cleared here.
17191725
// After move_legacy_finalizers(), unreachable is normal list.
1720-
move_legacy_finalizers(&unreachable, &finalizers);
1726+
Py_ssize_t unreachable_count = move_legacy_finalizers(&unreachable, &finalizers);
1727+
17211728
/* finalizers contains the unreachable objects with a legacy finalizer;
17221729
* unreachable objects reachable *from* those are also uncollectable,
17231730
* and we move those into the finalizers list too.
@@ -1734,6 +1741,7 @@ gc_collect_region(PyThreadState *tstate,
17341741

17351742
/* Clear weakrefs and invoke callbacks as necessary. */
17361743
stats->collected += handle_weakrefs(&unreachable, to);
1744+
17371745
gc_list_validate_space(to, gcstate->visited_space);
17381746
validate_list(to, collecting_clear_unreachable_clear);
17391747
validate_list(&unreachable, collecting_set_unreachable_clear);
@@ -1750,15 +1758,17 @@ gc_collect_region(PyThreadState *tstate,
17501758
gc_list_init(&final_unreachable);
17511759
if (check_resurrected) {
17521760
handle_resurrected_objects(&unreachable, &final_unreachable, to);
1761+
unreachable_count = gc_list_size(&final_unreachable);
17531762
} else {
17541763
gc_list_merge(&unreachable, &final_unreachable);
1764+
assert(unreachable_count == gc_list_size(&final_unreachable));
17551765
}
17561766

17571767
/* Call tp_clear on objects in the final_unreachable set. This will cause
17581768
* the reference cycles to be broken. It may also cause some objects
17591769
* in finalizers to be freed.
17601770
*/
1761-
stats->collected += gc_list_size(&final_unreachable);
1771+
stats->collected += unreachable_count;
17621772
delete_garbage(tstate, gcstate, &final_unreachable, to);
17631773

17641774
/* Collect statistics on uncollectable objects found and print

0 commit comments

Comments
 (0)