Skip to content

gc.get_count() can starve GC scheduling on the free-threaded build #157377

Description

@ngoldbaum

Bug description:

On the free-threaded build, a thread that calls gc.get_count() in an allocation loop never triggers a cyclic collection, so cyclic garbage is never collected and memory grows without bound. The GIL build is unaffected.

import gc, sys

def loop(poll):
    gc.collect()
    before = gc.get_stats()[0]["collections"]
    blocks_before = sys.getallocatedblocks()
    for _ in range(200_000):
        a = []
        a.append(a)              # one cyclic object, immediately unreachable
        if poll:
            gc.get_count()       # drains this thread's local allocation counter
    collections = gc.get_stats()[0]["collections"] - before
    print(f"poll_get_count={poll!s:<5}  collections_during_loop={collections:<4}  "
          f"young_count={gc.get_count()[0]:<7}  "
          f"allocated_blocks_grew_by={sys.getallocatedblocks() - blocks_before}")

loop(poll=False)
loop(poll=True)

Output on Python 3.15.0rc2 free-threading build (macOS arm64):

poll_get_count=False  collections_during_loop=93    young_count=801      allocated_blocks_grew_by=1788
poll_get_count=True   collections_during_loop=0     young_count=199624   allocated_blocks_grew_by=400009

With gc.get_count() in the loop, zero collections run and the ~200,000 cyclic objects are never freed; without it, the collector runs and memory stays bounded. The GIL build runs 88 collections either way.

record_allocation() is the only place that schedules a collection on the allocation path, and only when a thread's local counter reaches LOCAL_ALLOC_COUNT_THRESHOLD (512). gc.get_count() flushes that same counter to the global count and resets it to zero without doing the scheduling check, so frequent callers never reach the threshold and never schedule a collection.

cc @kumaraditya303 @nascheme

CPython versions tested on:

3.14, 3.15, CPython main branch

Operating systems tested on:

macOS

Linked PRs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions