Skip to content

Add freelists for PyComplex, PyInt, PyRange#7345

Merged
youknowone merged 6 commits intoRustPython:mainfrom
youknowone:freelist-more-types
Mar 5, 2026
Merged

Add freelists for PyComplex, PyInt, PyRange#7345
youknowone merged 6 commits intoRustPython:mainfrom
youknowone:freelist-more-types

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Mar 4, 2026

Summary

  • Add freelists for 4 more types matching CPython: PyComplex(100), PyInt(100), PyBoundMethod(20), PyRange(6)
  • Use try_with instead of with in all freelist push/pop (including existing float/list/dict/slice) to gracefully handle thread-local access during thread teardown

Follow-up to #7337.

Test plan

  • cargo build --release
  • cargo clippy clean
  • test_complex, test_int, test_range, test_class, test_descr all pass

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Performance

    • Improved memory efficiency for numeric and collection objects through optimized object reuse mechanisms.
  • Bug Fixes

    • Enhanced error handling in memory operations to gracefully degrade under resource contention instead of crashing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces per-type freelist mechanisms for PyComplex, PyInt, and PyRange objects to enable memory reuse, while improving error handling in existing freelists for dict, float, list, and slice by replacing panicking .with() calls with fallible .try_with() patterns. FreeList infrastructure is enhanced with safer memory deallocation and ergonomic access methods.

Changes

Cohort / File(s) Summary
New Per-Type Freelists
crates/vm/src/builtins/complex.rs, crates/vm/src/builtins/int.rs, crates/vm/src/builtins/range.rs
Introduces thread-local freelist storage (COMPLEX_FREELIST, INT_FREELIST, RANGE_FREELIST) and implements PyPayload constants MAX_FREELIST/HAS_FREELIST and unsafe freelist_push/freelist_pop methods for object reuse. Range uses MAX_FREELIST=6 while others use 100.
Improved Error Handling in Existing Freelists
crates/vm/src/builtins/dict.rs, crates/vm/src/builtins/float.rs, crates/vm/src/builtins/list.rs, crates/vm/src/builtins/slice.rs
Changes freelist access from .with() to .try_with() for graceful failure handling; freelist_push now returns false and freelist_pop returns None when lock acquisition fails instead of panicking.
FreeList Infrastructure
crates/vm/src/object/core.rs
Adds const fn new() constructor to FreeList, implements Deref and DerefMut traits for ergonomic Vec access, and replaces drop behavior to deallocate raw memory without invoking destructors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Freelists bloom in every type,
Complex, Int, and Range take flight,
No more panic when locks collide,
With try_with's graceful stride,
Memory reuse, objects thrive! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title states 'Add freelists for PyComplex, PyInt, PyRange' but the PR also adds a freelist for PyBoundMethod, which is not mentioned in the title. Update the title to include PyBoundMethod, such as 'Add freelists for PyComplex, PyInt, PyBoundMethod, PyRange' or 'Add freelists for additional types: PyComplex, PyInt, PyBoundMethod, PyRange'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.77% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d48faafdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.try_with(|fl| {
let mut list = fl.take();
let stored = if list.len() < Self::MAX_FREELIST {
list.push(obj);

Choose a reason for hiding this comment

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

P1 Badge Drop bound-method payload refs before freelist caching

Pushing PyBoundMethod objects into the freelist here keeps their object and function PyObjectRefs alive until that freelist slot is reused (or thread teardown), because PyBoundMethod does not provide a payload clear step that releases those references before list.push(obj). In practice, creating and dropping bound methods can now delay destruction/finalizers of arbitrary user objects and retain resources unexpectedly when the freelist is not churned.

Useful? React with 👍 / 👎.

.try_with(|fl| {
let mut list = fl.take();
let stored = if list.len() < Self::MAX_FREELIST {
list.push(obj);

Choose a reason for hiding this comment

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

P2 Badge Clear range endpoints before caching range objects

PyRange now enters a freelist, but each cached object still owns start/stop/step PyIntRefs because there is no clear path before list.push(obj). This means dropped ranges can retain large bigint endpoint allocations until the freelist slot is reused (or thread exit), creating avoidable memory retention spikes in workloads that create large ranges.

Useful? React with 👍 / 👎.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/builtins/function.rs`:
- Around line 1179-1222: PyBoundMethod currently uses the auto-derived Traverse
but lacks a manual Traverse::clear implementation, so freelisted PyBoundMethod
objects retain strong refs to their object and function fields and block GC;
implement a manual impl Traverse for PyBoundMethod that provides a clear(&self)
(or clear(&mut self) following the crate pattern) which detaches/releases the
strong references (set object and function fields to
PyObjectRef::none()/ptr::null or equivalent detach method used by
PyFunction/PyTuple), then ensure freelist_push stores only cleared objects so
freelisted PyBoundMethod will not keep references alive (update the impl for
PyPayload::clear/Traverse for PyBoundMethod to match the existing pattern used
by PyFunction, PyTuple, PyList, PyDict).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 2ec1868c-eea8-4060-bf02-28ec5400c8d0

📥 Commits

Reviewing files that changed from the base of the PR and between a98c646 and 8d48faa.

📒 Files selected for processing (8)
  • crates/vm/src/builtins/complex.rs
  • crates/vm/src/builtins/dict.rs
  • crates/vm/src/builtins/float.rs
  • crates/vm/src/builtins/function.rs
  • crates/vm/src/builtins/int.rs
  • crates/vm/src/builtins/list.rs
  • crates/vm/src/builtins/range.rs
  • crates/vm/src/builtins/slice.rs

Comment on lines 1179 to 1222
// spell-checker:ignore MAXFREELIST
thread_local! {
static BOUND_METHOD_FREELIST: Cell<crate::object::FreeList<PyBoundMethod>> = const { Cell::new(crate::object::FreeList::new()) };
}

impl PyPayload for PyBoundMethod {
const MAX_FREELIST: usize = 20;
const HAS_FREELIST: bool = true;

#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.bound_method_type
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
BOUND_METHOD_FREELIST
.try_with(|fl| {
let mut list = fl.take();
let stored = if list.len() < Self::MAX_FREELIST {
list.push(obj);
true
} else {
false
};
fl.set(list);
stored
})
.unwrap_or(false)
}

#[inline]
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
BOUND_METHOD_FREELIST
.try_with(|fl| {
let mut list = fl.take();
let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
fl.set(list);
result
})
.ok()
.flatten()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PyBoundMethod freelist + traverse declarations =="
rg -n "name = \"method\", traverse|unsafe impl Traverse for PyBoundMethod|BOUND_METHOD_FREELIST" \
  crates/vm/src/builtins/function.rs -C4

echo
echo "== Traverse clear implementations for object refs =="
rg -n "impl .*Traverse for PyObjectRef|impl .*Traverse for Option<PyObjectRef>|fn clear\\(" \
  crates/vm/src -g '*.rs' -C4

echo
echo "== Dealloc/freelist flow in object core =="
rg -n "tp_clear|freelist_push|drop\\(edges\\)|freelist_pop|drop_in_place" \
  crates/vm/src/object/core.rs -C5

Repository: RustPython/RustPython

Length of output: 16552


Implement explicit Traverse::clear for PyBoundMethod to detach refs before freelist storage.

PyBoundMethod uses auto traverse (line 1066) but lacks a manual clear() implementation. The default clear() does nothing, so the object and function strong references remain embedded in freelisted bound methods. This blocks garbage collection of referenced objects.

The fix is straightforward: implement manual Traverse with an explicit clear() that detaches both fields, matching the pattern already established for PyFunction and other ref-bearing types (PyTuple, PyList, PyDict, etc.).

Proposed fix
-#[pyclass(module = false, name = "method", traverse)]
+#[pyclass(module = false, name = "method", traverse = "manual")]
 #[derive(Debug)]
 pub struct PyBoundMethod {
     object: PyObjectRef,
     function: PyObjectRef,
 }
+
+unsafe impl Traverse for PyBoundMethod {
+    fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) {
+        self.object.traverse(traverse_fn);
+        self.function.traverse(traverse_fn);
+    }
+
+    fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
+        out.push(core::mem::replace(
+            &mut self.object,
+            Context::genesis().none.to_owned().into(),
+        ));
+        out.push(core::mem::replace(
+            &mut self.function,
+            Context::genesis().none.to_owned().into(),
+        ));
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/builtins/function.rs` around lines 1179 - 1222, PyBoundMethod
currently uses the auto-derived Traverse but lacks a manual Traverse::clear
implementation, so freelisted PyBoundMethod objects retain strong refs to their
object and function fields and block GC; implement a manual impl Traverse for
PyBoundMethod that provides a clear(&self) (or clear(&mut self) following the
crate pattern) which detaches/releases the strong references (set object and
function fields to PyObjectRef::none()/ptr::null or equivalent detach method
used by PyFunction/PyTuple), then ensure freelist_push stores only cleared
objects so freelisted PyBoundMethod will not keep references alive (update the
impl for PyPayload::clear/Traverse for PyBoundMethod to match the existing
pattern used by PyFunction, PyTuple, PyList, PyDict).

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

Code has been automatically formatted

The code in this PR has been formatted using:

  • cargo fmt --all
    Please pull the latest changes before pushing again:
git pull origin freelist-more-types

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/vm/src/object/core.rs (1)

825-830: ⚠️ Potential issue | 🟡 Minor

Update the FreeList doc comment to match the new drop behavior.

The comment says Drop converts pointers back to Box<PyInner<T>> for proper deallocation, but the implementation now does raw dealloc without running destructors. This is now factually incorrect.

Proposed doc fix
-/// Wraps a `Vec<*mut PyObject>` and implements `Drop` to convert each
-/// raw pointer back into `Box<PyInner<T>>` for proper deallocation.
+/// Wraps a `Vec<*mut PyObject>` and implements `Drop` to free raw
+/// `PyInner<T>` allocations during thread teardown. Payload destructors are
+/// intentionally skipped to avoid TLS-after-destruction access.

As per coding guidelines, "Do not delete or rewrite existing comments unless they are factually wrong or directly contradict the new code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/object/core.rs` around lines 825 - 830, Update the FreeList doc
comment to reflect the current Drop behavior: replace the statement that Drop
"converts each raw pointer back into Box<PyInner<T>> for proper deallocation"
with a precise description that Drop performs raw deallocation of the stored
pointers without running their Rust destructors (i.e., it calls dealloc on the
raw allocation), mention that this is unsafe and intentionally avoids destructor
execution, and reference the FreeList<T> type and its Drop implementation so
readers understand the semantic change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/object/core.rs`:
- Around line 852-861: The current teardown loop in self.items.drain(..)
directly deallocates the raw PyInner<T> buffer (Layout::new::<PyInner<T>>())
which skips payload destructors and allows unbounded byte/heap leaks for types
with large owned payloads; change it so teardown only raw-deallocates when it is
provably safe and otherwise drops or scrubs payloads: add a marker trait (e.g.,
SafeToTearDown) or a per-type flag and, inside the loop over self.items, for
items where T: SafeToTearDown use ptr::drop_in_place or ptr::read to run
destructor on PyInner<T> (dropping owned fields), and only fall back to
alloc::alloc::dealloc for types not safe to drop; alternatively enforce freelist
policy per type by limiting capacity for non-SafeToTearDown types before
teardown. Ensure references to self.items.drain(..), PyInner<T>, and
Layout::new::<PyInner<T>>() are updated accordingly.

---

Outside diff comments:
In `@crates/vm/src/object/core.rs`:
- Around line 825-830: Update the FreeList doc comment to reflect the current
Drop behavior: replace the statement that Drop "converts each raw pointer back
into Box<PyInner<T>> for proper deallocation" with a precise description that
Drop performs raw deallocation of the stored pointers without running their Rust
destructors (i.e., it calls dealloc on the raw allocation), mention that this is
unsafe and intentionally avoids destructor execution, and reference the
FreeList<T> type and its Drop implementation so readers understand the semantic
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9f925e76-bdd3-4670-94c9-bb20c2b3d0b8

📥 Commits

Reviewing files that changed from the base of the PR and between 8d48faa and 67da7b0.

📒 Files selected for processing (1)
  • crates/vm/src/object/core.rs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/vm/src/object/core.rs (1)

852-861: ⚠️ Potential issue | 🟠 Major

Leak is only bounded by object count, not by memory bytes.

Line 856-857 says leakage is bounded by MAX_FREELIST, but this path skips Drop for payloads, so leaked heap bytes per entry can still be large (e.g., big integer/container internals). Please add a teardown policy that either drops payloads for proven-safe types or avoids raw-dealloc teardown for types with unbounded payload memory.

As per coding guidelines, "Follow Rust best practices for error handling and memory management".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/object/core.rs` around lines 852 - 861, The teardown loop in
self.items.drain(..) unsafely deallocates PyInner<T> bytes without running
payload Drop, so the claimed bound by MAX_FREELIST is incorrect because payloads
may own unbounded heap memory; change the teardown to detect types whose
payloads may allocate (or implement Drop) and for those either call their
destructor (run Drop) or use a safe per-type policy that moves items into a safe
drop path (e.g., call a provided safe_free/drop_fn for the type) instead of raw
dealloc; locate and modify the loop that deallocates with
alloc::alloc::dealloc(ptr as *mut u8, Layout::new::<PyInner<T>>()) and the
PyInner<T> handling to branch on a trait/flag (e.g., implements
Drop/HasHeapPayload) or whitelist-only raw dealloc for truly fixed-size types to
avoid leaking unbounded heap bytes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/vm/src/object/core.rs`:
- Around line 852-861: The teardown loop in self.items.drain(..) unsafely
deallocates PyInner<T> bytes without running payload Drop, so the claimed bound
by MAX_FREELIST is incorrect because payloads may own unbounded heap memory;
change the teardown to detect types whose payloads may allocate (or implement
Drop) and for those either call their destructor (run Drop) or use a safe
per-type policy that moves items into a safe drop path (e.g., call a provided
safe_free/drop_fn for the type) instead of raw dealloc; locate and modify the
loop that deallocates with alloc::alloc::dealloc(ptr as *mut u8,
Layout::new::<PyInner<T>>()) and the PyInner<T> handling to branch on a
trait/flag (e.g., implements Drop/HasHeapPayload) or whitelist-only raw dealloc
for truly fixed-size types to avoid leaking unbounded heap bytes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c7efdfae-3fc0-436a-88ce-d048138313fd

📥 Commits

Reviewing files that changed from the base of the PR and between 67da7b0 and 8b4efc1.

📒 Files selected for processing (1)
  • crates/vm/src/object/core.rs

@youknowone
Copy link
Member Author

Review response

Applied

  1. FreeList doc comment (CodeRabbit): Updated to reflect actual Drop behavior — raw dealloc without running destructors to avoid TLS-after-destruction.

  2. PyBoundMethod clear = false (CodeRabbit Critical / Codex P1): Added clear = false to #[pyclass] attribute. CPython's classobject.c has tp_clear = 0 (NULL) for bound method objects — they are traversed but never cleared.

Not applied (with reasoning)

  1. PyRange clear (Codex P2): CPython's rangeobject.c has both tp_traverse = 0 and tp_clear = 0 — range objects are not GC-tracked at all. Our PyRange doesn't have traverse either, so no clear is needed.

  2. FreeList::Drop leak concern (CodeRabbit Major): The raw dealloc path only runs during thread teardown, not during normal object lifecycle. During normal operation, objects are properly deallocated with full destructors. Thread teardown leaks are bounded and intentional — same design as CPython which also doesn't free freelist objects on thread destruction.

commented by Claude

@youknowone youknowone force-pushed the freelist-more-types branch from 8617ce6 to 2499e1c Compare March 4, 2026 16:54
@youknowone youknowone changed the title Add freelists for PyComplex, PyInt, PyBoundMethod, PyRange Add freelists for PyComplex, PyInt, PyRange Mar 4, 2026
youknowone and others added 6 commits March 5, 2026 02:02
- PyComplex(100), PyInt(100), PyBoundMethod(20), PyRange(6)
- Use try_with instead of with in all freelist push/pop to prevent
  panic on thread-local access during thread teardown
During thread teardown, Box::from_raw triggers cascading destructors
that may access already-destroyed thread-local storage (GC state, other
freelists). Use raw dealloc instead to free memory without running
destructors.
- Set clear=false on PyBoundMethod (tp_clear=NULL in classobject.c)
- Update FreeList doc comment to match actual Drop behavior (raw dealloc)
Non-Option PyObjectRef fields retain references in freelist, causing
weakref and refcount assertions to fail in test_unittest, test_multiprocessing,
and test_socket.
@youknowone youknowone force-pushed the freelist-more-types branch from 2499e1c to 01b5319 Compare March 4, 2026 17:02
@youknowone youknowone merged commit 86134e1 into RustPython:main Mar 5, 2026
33 of 35 checks passed
@youknowone youknowone deleted the freelist-more-types branch March 5, 2026 11:09
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