Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Remove freelist_hint; call freelist_push before tp_clear
By calling freelist_push before tp_clear, the payload is still intact
and can be read directly (e.g. tuple element count for bucket selection).
This eliminates freelist_hint and the hint parameter entirely.
  • Loading branch information
youknowone committed Mar 9, 2026
commit c32f31bbd6ca96eb01a060948c8233ce7bc2a456
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl PyPayload for PyComplex {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
COMPLEX_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ impl PyPayload for PyDict {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
DICT_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl PyPayload for PyFloat {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
FLOAT_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl PyPayload for PyInt {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
INT_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl PyPayload for PyList {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
LIST_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl PyPayload for PyRange {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
RANGE_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl PyPayload for PySlice {
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
SLICE_FREELIST
.try_with(|fl| {
let mut list = fl.take();
Expand Down
10 changes: 2 additions & 8 deletions crates/vm/src/builtins/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,8 @@ impl PyPayload for PyTuple {
}

#[inline]
unsafe fn freelist_hint(obj: *mut PyObject) -> usize {
let py_tuple = unsafe { &*(obj as *const crate::Py<PyTuple>) };
py_tuple.elements.len()
}

#[inline]
unsafe fn freelist_push(obj: *mut PyObject, hint: usize) -> bool {
let len = hint;
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
let len = unsafe { &*(obj as *const crate::Py<PyTuple>) }.elements.len();
if len == 0 || len > TupleFreeList::MAX_SAVE_SIZE {
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
29 changes: 11 additions & 18 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,39 +188,32 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
);
}

// Capture freelist bucket hint before tp_clear empties the payload.
// Size-based freelists (e.g. PyTuple) need the element count for bucket selection,
// but clear() replaces elements with an empty slice.
let freelist_hint = if T::HAS_FREELIST {
unsafe { T::freelist_hint(obj) }
} else {
0
};

// Extract child references before deallocation to break circular refs (tp_clear)
let mut edges = Vec::new();
if let Some(clear_fn) = vtable.clear {
unsafe { clear_fn(obj, &mut edges) };
}

// Try to store in freelist for reuse; otherwise deallocate.
// Try to store in freelist for reuse BEFORE tp_clear, so that
// size-based freelists (e.g. PyTuple) can read the payload directly.
// Only exact base types (not heaptype or structseq subtypes) go into the freelist.
let typ = obj_ref.class();
let pushed = if T::HAS_FREELIST
&& typ.heaptype_ext.is_none()
&& core::ptr::eq(typ, T::class(crate::vm::Context::genesis()))
{
unsafe { T::freelist_push(obj, freelist_hint) }
unsafe { T::freelist_push(obj) }
} else {
false
};

// Extract child references to break circular refs (tp_clear).
// This runs regardless of freelist push — the object's children must be released.
let mut edges = Vec::new();
if let Some(clear_fn) = vtable.clear {
unsafe { clear_fn(obj, &mut edges) };
}

if !pushed {
// Deallocate the object memory (handles ObjExt prefix if present)
unsafe { PyInner::dealloc(obj as *mut PyInner<T>) };
}

// Drop child references - may trigger recursive destruction.
// The object is already deallocated, so circular refs are broken.
drop(edges);

// Trashcan: decrement depth and process deferred objects at outermost level
Expand Down
19 changes: 4 additions & 15 deletions crates/vm/src/object/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,15 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static {
/// Maximum number of objects to keep in the freelist.
const MAX_FREELIST: usize = 0;

/// Capture a hint value from the payload before tp_clear runs.
/// Used by size-based freelists (e.g. PyTuple) to remember the element
/// count before clear empties the payload.
///
/// # Safety
/// `obj` must be a valid pointer to a `PyInner<Self>` with the payload still intact.
#[inline]
unsafe fn freelist_hint(_obj: *mut PyObject) -> usize {
0
}

/// Try to push a dead object onto this type's freelist for reuse.
/// Returns true if the object was stored (caller must NOT free the memory).
/// `hint` is the value returned by `freelist_hint` before tp_clear.
/// Called before tp_clear, so the payload is still intact.
///
/// # Safety
/// `obj` must be a valid pointer to a `PyInner<Self>` with refcount 0,
/// after `drop_slow_inner` and `tp_clear` have already run.
/// `obj` must be a valid pointer to a `PyInner<Self>` with refcount 0.
/// The payload is still initialized and can be read for bucket selection.
#[inline]
unsafe fn freelist_push(_obj: *mut PyObject, _hint: usize) -> bool {
unsafe fn freelist_push(_obj: *mut PyObject) -> bool {
false
}

Expand Down
Loading