Add freelists for PyComplex, PyInt, PyRange (#7345)

* Add freelists for PyComplex, PyInt, PyBoundMethod, PyRange

- 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

* Use alloc::dealloc in FreeList::Drop to avoid thread-local access panic

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.

* Auto-format: cargo fmt --all

* Fix clippy: use core::alloc::Layout instead of alloc::alloc::Layout

* Address review: PyBoundMethod clear=false, update FreeList doc comment

- Set clear=false on PyBoundMethod (tp_clear=NULL in classobject.c)
- Update FreeList doc comment to match actual Drop behavior (raw dealloc)

* Remove PyBoundMethod freelist to fix refcount/weakref test failures

Non-Option PyObjectRef fields retain references in freelist, causing
weakref and refcount assertions to fail in test_unittest, test_multiprocessing,
and test_socket.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Jeong, YunWonandgithub-actions[bot] authored and GitHub committed 2026-03-05 20:09:26 +09:00
1 parent ce76a7db61
commit e51248f779
8 files changed
+221 -73

No files matched your search

+40
View File
@@ -10,7 +10,9 @@ use crate::{
stdlib::warnings,
types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
};
use core::cell::Cell;
use core::num::Wrapping;
use core::ptr::NonNull;
use num_complex::Complex64;
use num_traits::Zero;
use rustpython_common::hash;
@@ -24,11 +26,49 @@ pub struct PyComplex {
value: Complex64,
}
// spell-checker:ignore MAXFREELIST
thread_local! {
static COMPLEX_FREELIST: Cell<crate::object::FreeList<PyComplex>> = const { Cell::new(crate::object::FreeList::new()) };
}
impl PyPayload for PyComplex {
const MAX_FREELIST: usize = 100;
const HAS_FREELIST: bool = true;
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.complex_type
}
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
COMPLEX_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>> {
COMPLEX_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()
}
}
impl ToPyObject for Complex64 {
+22 -17
View File
@@ -77,27 +77,32 @@ impl PyPayload for PyDict {
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
DICT_FREELIST.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
})
DICT_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>> {
DICT_FREELIST.with(|fl| {
let mut list = fl.take();
let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
fl.set(list);
result
})
DICT_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()
}
}
+22 -17
View File
@@ -49,27 +49,32 @@ impl PyPayload for PyFloat {
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
FLOAT_FREELIST.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
})
FLOAT_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>> {
FLOAT_FREELIST.with(|fl| {
let mut list = fl.take();
let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
fl.set(list);
result
})
FLOAT_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()
}
}
+40
View File
@@ -20,7 +20,9 @@ use crate::{
types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
};
use alloc::fmt;
use core::cell::Cell;
use core::ops::{Neg, Not};
use core::ptr::NonNull;
use malachite_bigint::{BigInt, Sign};
use num_integer::Integer;
use num_traits::{One, Pow, PrimInt, Signed, ToPrimitive, Zero};
@@ -48,7 +50,15 @@ where
}
}
// spell-checker:ignore MAXFREELIST
thread_local! {
static INT_FREELIST: Cell<crate::object::FreeList<PyInt>> = const { Cell::new(crate::object::FreeList::new()) };
}
impl PyPayload for PyInt {
const MAX_FREELIST: usize = 100;
const HAS_FREELIST: bool = true;
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.int_type
@@ -57,6 +67,36 @@ impl PyPayload for PyInt {
fn into_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.new_int(self.value).into()
}
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
INT_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>> {
INT_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()
}
}
macro_rules! impl_into_pyobject_int {
+22 -17
View File
@@ -89,27 +89,32 @@ impl PyPayload for PyList {
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
LIST_FREELIST.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
})
LIST_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>> {
LIST_FREELIST.with(|fl| {
let mut list = fl.take();
let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
fl.set(list);
result
})
LIST_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()
}
}
+40
View File
@@ -15,7 +15,9 @@ use crate::{
Representable, SelfIter,
},
};
use core::cell::Cell;
use core::cmp::max;
use core::ptr::NonNull;
use crossbeam_utils::atomic::AtomicCell;
use malachite_bigint::{BigInt, Sign};
use num_integer::Integer;
@@ -67,11 +69,49 @@ pub struct PyRange {
pub step: PyIntRef,
}
// spell-checker:ignore MAXFREELIST
thread_local! {
static RANGE_FREELIST: Cell<crate::object::FreeList<PyRange>> = const { Cell::new(crate::object::FreeList::new()) };
}
impl PyPayload for PyRange {
const MAX_FREELIST: usize = 6;
const HAS_FREELIST: bool = true;
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.range_type
}
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
RANGE_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>> {
RANGE_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()
}
}
impl PyRange {
+22 -17
View File
@@ -60,27 +60,32 @@ impl PyPayload for PySlice {
#[inline]
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
SLICE_FREELIST.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
})
SLICE_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>> {
SLICE_FREELIST.with(|fl| {
let mut list = fl.take();
let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
fl.set(list);
result
})
SLICE_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()
}
}
+13 -5
View File
@@ -858,11 +858,11 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
}
}
/// Thread-local freelist storage that properly deallocates cached objects
/// on thread teardown.
/// Thread-local freelist storage for reusing object allocations.
///
/// 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>`. On thread teardown, `Drop` frees raw
/// `PyInner<T>` allocations without running payload destructors to avoid
/// accessing already-destroyed thread-local storage (GC state, other freelists).
pub(crate) struct FreeList<T: PyPayload> {
items: Vec<*mut PyObject>,
_marker: core::marker::PhantomData<T>,
@@ -885,8 +885,16 @@ impl<T: PyPayload> Default for FreeList<T> {
impl<T: PyPayload> Drop for FreeList<T> {
fn drop(&mut self) {
// During thread teardown, we cannot safely run destructors on cached
// objects because their Drop impls may access thread-local storage
// (GC state, other freelists) that is already destroyed.
// Instead, free just the raw allocation. The payload's heap fields
// (BigInt, PyObjectRef, etc.) are leaked, but this is bounded by
// MAX_FREELIST per type per thread.
for ptr in self.items.drain(..) {
drop(unsafe { Box::from_raw(ptr as *mut PyInner<T>) });
unsafe {
alloc::alloc::dealloc(ptr as *mut u8, core::alloc::Layout::new::<PyInner<T>>());
}
}
}
}