@@ -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
580580move_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
11751165by a call to 'move_legacy_finalizers'), the 'unreachable' list is not a normal
11761166list and we can not use most gc_list_* functions for it. */
11771167static 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