Skip to content

Fix GC tracking and capacity of pyclass freelists - #6442

Open
Zaczero wants to merge 2 commits into
PyO3:mainfrom
Zaczero:pr/freelist-correctness
Open

Zaczero wants to merge 2 commits into
PyO3:mainfrom
Zaczero:pr/freelist-correctness

Conversation

@Zaczero

@Zaczero Zaczero commented Sep 20, 2026

Copy link
Copy Markdown

Impact

Objects reused by #[pyclass(freelist = N)] can remain untracked by CPython's cyclic garbage collector. If a reused object enters a reference cycle, dropping its external references and calling gc.collect() does not reclaim it. The cycle can retain Python objects and Rust-owned resources in long-running processes.

The freelist also retains only N - 1 allocations. In particular, freelist = 1 never caches an allocation. This PR fixes both correctness issues in separate commits, preserving the existing cache implementation.

Approach

PyObject_Init restores the object header and type reference, but does not restore GC tracking. Tracking is restored at the complete-instance initialization boundary, after every recursive Rust base initializer has finished. Fresh allocations and native-base instances that are already tracked retain that state.

The insertion bound is corrected to admit all N entries. Capacity zero remains valid, and overflow returns the allocation to the caller.

Regression coverage

  • freelist_reuse_is_tracked_and_collectible primes and confirms a cache hit, checks GC tracking, creates a self-cycle, and observes eventual destruction through the suite's bounded free-threading-aware collection helper. It failed on the original implementation.
  • freelist_reuse_initializes_all_rust_bases deterministically confirms reuse before covering recursive Rust initialization, instance-dictionary reset, weak references, and eventual cycle collection outside the attachment scope. It also failed before the lifecycle fix.
  • exact_capacity_and_lifo exercises capacities 0, 1, 2, and 17 through empty, full, overflow, refill, and reverse-order removal.
  • class_with_freelist retains its existing recycling and distinct-live-allocation checks, with the declared capacity changed to one.

The GC suite and freelist unit tests cover 28 cases on CPython 3.14.7 and 28 on its free-threaded build. The no-std configuration covers 12 applicable cases. All completed successfully.

The shared-process GC suite also covers 27 cases on free-threaded CPython 3.15 development. That configuration exposed and now guards against assuming that one gc.collect() call is a destruction barrier while collection is concurrent.

The capacity unit test checks the insertion bound independently. Disabling only GC re-tracking, with the capacity fix retained, makes both GC regressions fail at their tracking assertions.

Validation commands
PYO3_PYTHON=python3.14 cargo nextest run -p pyo3 \
  --features full,auto-initialize --lib --test test_gc \
  -E 'test(freelist) | binary(test_gc)'

PYO3_PYTHON=python3.14t PYTHON_GIL=0 cargo nextest run -p pyo3 \
  --features full,auto-initialize --lib --test test_gc \
  -E 'test(freelist) | binary(test_gc)'

PYO3_WIP_NO_STD=1 PYO3_PYTHON=python3.14t PYTHON_GIL=0 \
  cargo nextest run -p pyo3 --features auto-initialize,parking_lot,hashbrown \
  --lib --test test_gc -E 'test(freelist) | binary(test_gc)'

These lifecycle changes were also exercised across CPython 3.9–3.15, debug Python, and stable-ABI builds.

A release note is included.

Related to #6133: these defects were found while establishing a correct baseline for the allocation-cache performance investigation. This PR addresses correctness independently of the proposed optimization.

AI assistance was used for investigation, implementation, and test preparation.

@Zaczero
Zaczero force-pushed the pr/freelist-correctness branch from 0ee539e to 8807eaf Compare September 20, 2026 10:55
@codspeed

codspeed Bot commented Sep 20, 2026

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
❌ 2 regressed benchmarks
✅ 135 untouched benchmarks
⏩ 6 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
into_i128_zero 817.2 ns 982.4 ns -16.82%
test_empty_class_init 28.5 µs 31.7 µs -10.05%
extract_u128_small 1.9 µs 1.6 µs +13.4%
extract_u128_zero 1.9 µs 1.7 µs +12.97%
into_u128_small 987.2 ns 874.7 ns +12.86%
extract_u128_u32_max 2.1 µs 1.8 µs +12.03%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing Zaczero:pr/freelist-correctness (1516200) with main (52a646f)

Open in CodSpeed

Footnotes

  1. 6 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Complete all Rust base initializers before restoring GC tracking. Fresh allocations and native constructors which already track the instance retain their existing state.

Add regression coverage for confirmed cache hits, cycle collection, inherited Rust payloads, dictionaries, and weak references.
@Zaczero
Zaczero force-pushed the pr/freelist-correctness branch 2 times, most recently from 67f77d1 to 2e1df45 Compare September 20, 2026 13:09
Allow all N slots to retain allocations, including a one-entry freelist. Cover empty, full, overflow, refill, and LIFO behavior, and correct the reuse test which depended on the old N - 1 limit.
@Zaczero
Zaczero force-pushed the pr/freelist-correctness branch from 2e1df45 to 1516200 Compare September 20, 2026 13:15
@Zaczero
Zaczero marked this pull request as ready for review September 20, 2026 13:53

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant