Skip to content

gh-159437: Support frozen objects in combined GC reference passes - #159597

Draft
mfalco-oai wants to merge 7 commits into
python:mainfrom
mfalco-oai:mfalco-oai/gh-159437-gc-frozen-fusion
Draft

mfalco-oai wants to merge 7 commits into
python:mainfrom
mfalco-oai:mfalco-oai/gh-159437-gc-frozen-fusion

Conversation

@mfalco-oai

@mfalco-oai mfalco-oai commented Oct 16, 2026 •

Copy link
Copy Markdown

With this optimization the GIL collector can use the combined reference pass during the initial reachability analysis of full collections with frozen objects, on builds with 64-bit pointers. Bit 2 of _gc_prev identifies objects owned by the permanent generation, so the pass leaves their temporary reference state untouched.

The marker requires a minimum alignment of 8 bytes for exposed object layouts; opaque headers keep _PyObject_MIN_ALIGNMENT at 4. gc.freeze() and gc.unfreeze() walk their lists to set and clear the marker. Frozen weakrefs retain it while queued for callbacks, and surviving weakrefs have it cleared before moving to the old generation.

The guard for subinterpreters and runtime reinitialization still applies.

Count the stable unreachable set while preparing callbacks and retain a conservative cleanup flag. Reuse the count only when late weakref clearing has no work, and force fresh cleanup and counting after resurrection analysis.

Add a regression for a callback-free weakref observed when clearing a young cycle releases an older finalizer.
Count tp_del candidates during the existing reference traversals so zero can skip legacy classification. Reuse callback preparation to clear initial unreachable tags, while preserving normalized links before DEBUG_COLLECTABLE output can invoke Python.

Cover debug visibility of weakrefs with callbacks and legacy classification during a generation 0 collection.
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 16, 2026

@pablogsal pablogsal left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave frozen support out of this series for now. The persistent header flag needs updating during freeze, unfreeze, untracking and weakref moves, so there is quite a bit more state to maintain here.

An earlier reference pass change I worked on kept the ordinary path when frozen objects were present. Keeping that fallback would let us handle frozen support separately once the application benefit is clearer.

I ran a little experiment keeping a cache of parsed SQL queries, freezing the runtime, then parsing and rendering more queries. Repeated runs on a busy machine didn't show a clear overall gain over the preceding change. The same work in a child after fork looked more promising, but there weren't enough runs to draw a conclusion.

Freezing and unfreezing that retained heap took much longer than with the fallback. Unfreezing in the child after fork was more expensive again. We need to include those costs when deciding whether this pays off.

@nascheme, any thoughts on the extra work in freeze() and unfreeze() here?

Comment thread Python/gc.c
if (wrcb_to_call->_gc_next == (uintptr_t)gc) {
#ifdef _PyGC_PREV_MASK_PERM
// The old generation must not retain permanent markers.
gc->_gc_prev &= ~_PyGC_PREV_MASK_PERM;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The weakref has already left the permanent generation when it enters wrcb_to_call. Why keep PERM set until after the callback? Clearing it when we queue the weakref would keep the flag tied to list membership.

Comment thread Lib/test/test_gc.py

gc.disable()
first = Node()
first.link = first

@pablogsal pablogsal Oct 17, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried leaving the frozen flag set after unfreeze to see whether this test caught it. These separate self cycles still passed. Making first and second point at each other after the frozen setup left the objects alive and caught the missing collection. We should use that cycle here.

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

Labels

type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants