mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
freelist optimization (#7337)
* Add object freelist infrastructure and PyFloat freelist Add freelist_push/freelist_pop hooks to PyPayload trait with default no-op implementations. Modify default_dealloc to offer dead objects to the freelist before freeing memory. Modify PyRef::new_ref to try popping from the freelist before allocating. Implement thread-local freelist for PyFloat (max 100 entries). Float objects are ideal candidates: fixed-size payload (f64), no GC tracking, no instance dict, trivial Drop. Benchmark improvement on float-heavy workloads: float_arith: ~12% faster float_list: ~17% faster * Add PyList freelist and GC-safe freelist infrastructure Add HAS_FREELIST const to PyPayload trait. For freelist types, GC untracking is done immediately (not deferred) during dealloc to prevent race conditions when the object is reused. Restructure new_ref so freelist-popped objects also get GC tracked, enabling freelist support for GC-tracked types like PyList. Add drop_in_place for old payload before writing new one, required for types with non-trivial Drop (e.g. PyList's RwLock<Vec>). Implement thread-local freelist for PyList (max 80 entries). * Add PyDict freelist and exact-type guard for freelist push Add thread-local freelist for PyDict (max 80 entries). Add heaptype check in default_dealloc to prevent subclass instances from entering the freelist. Subclass objects have a different Python type than the base type, so reusing them would return objects with the wrong __class__. Skip PyTuple freelist: structseq types (stat_result, struct_time) are static subtypes sharing the same Rust payload, making type-safe reuse impractical with heaptype check alone. * Add PySlice freelist (max 1, matching PySlice_MAXFREELIST) * Add FreeList<T> wrapper to drain cached objects on thread teardown Replace Cell<Vec<*mut PyObject>> with Cell<FreeList<T>> which implements Drop to properly deallocate PyInner<T> when threads exit. Also fix freelist_pop safety doc to match actual contract. * Add manual Traverse impl for PySlice with clear() and fix comment * Deduplicate allocation fallback in PyRef::new_ref
This commit is contained in:
1 parent
9899deba82
commit
af8f41ded1
8 files changed
+289
-12
No files matched your search
@@ -70,6 +70,7 @@ finalizers
|
||||
firsttraceable
|
||||
flowgraph
|
||||
formatfloat
|
||||
freelist
|
||||
freevar
|
||||
freevars
|
||||
fromlist
|
||||
|
||||
@@ -26,6 +26,8 @@ use crate::{
|
||||
vm::VirtualMachine,
|
||||
};
|
||||
use alloc::fmt;
|
||||
use core::cell::Cell;
|
||||
use core::ptr::NonNull;
|
||||
use rustpython_common::lock::PyMutex;
|
||||
use rustpython_common::wtf8::Wtf8Buf;
|
||||
|
||||
@@ -60,11 +62,43 @@ impl fmt::Debug for PyDict {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static DICT_FREELIST: Cell<crate::object::FreeList<PyDict>> = const { Cell::new(crate::object::FreeList::new()) };
|
||||
}
|
||||
|
||||
impl PyPayload for PyDict {
|
||||
const MAX_FREELIST: usize = 80;
|
||||
const HAS_FREELIST: bool = true;
|
||||
|
||||
#[inline]
|
||||
fn class(ctx: &Context) -> &'static Py<PyType> {
|
||||
ctx.types.dict_type
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PyDict {
|
||||
|
||||
@@ -15,6 +15,8 @@ use crate::{
|
||||
protocol::PyNumberMethods,
|
||||
types::{AsNumber, Callable, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
|
||||
};
|
||||
use core::cell::Cell;
|
||||
use core::ptr::NonNull;
|
||||
use malachite_bigint::{BigInt, ToBigInt};
|
||||
use num_complex::Complex64;
|
||||
use num_traits::{Signed, ToPrimitive, Zero};
|
||||
@@ -32,11 +34,43 @@ impl PyFloat {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static FLOAT_FREELIST: Cell<crate::object::FreeList<PyFloat>> = const { Cell::new(crate::object::FreeList::new()) };
|
||||
}
|
||||
|
||||
impl PyPayload for PyFloat {
|
||||
const MAX_FREELIST: usize = 100;
|
||||
const HAS_FREELIST: bool = true;
|
||||
|
||||
#[inline]
|
||||
fn class(ctx: &Context) -> &'static Py<PyType> {
|
||||
ctx.types.float_type
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPyObject for f64 {
|
||||
|
||||
@@ -27,7 +27,9 @@ use crate::{
|
||||
use rustpython_common::wtf8::Wtf8Buf;
|
||||
|
||||
use alloc::fmt;
|
||||
use core::cell::Cell;
|
||||
use core::ops::DerefMut;
|
||||
use core::ptr::NonNull;
|
||||
|
||||
#[pyclass(module = false, name = "list", unhashable = true, traverse = "manual")]
|
||||
#[derive(Default)]
|
||||
@@ -72,11 +74,43 @@ unsafe impl Traverse for PyList {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LIST_FREELIST: Cell<crate::object::FreeList<PyList>> = const { Cell::new(crate::object::FreeList::new()) };
|
||||
}
|
||||
|
||||
impl PyPayload for PyList {
|
||||
const MAX_FREELIST: usize = 80;
|
||||
const HAS_FREELIST: bool = true;
|
||||
|
||||
#[inline]
|
||||
fn class(ctx: &Context) -> &'static Py<PyType> {
|
||||
ctx.types.list_type
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPyObject for Vec<PyObjectRef> {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// sliceobject.{h,c} in CPython
|
||||
// spell-checker:ignore sliceobject
|
||||
use core::cell::Cell;
|
||||
use core::ptr::NonNull;
|
||||
use rustpython_common::wtf8::{Wtf8Buf, wtf8_concat};
|
||||
|
||||
use super::{PyGenericAlias, PyStrRef, PyTupleRef, PyType, PyTypeRef};
|
||||
@@ -15,7 +17,7 @@ use crate::{
|
||||
use malachite_bigint::{BigInt, ToBigInt};
|
||||
use num_traits::{One, Signed, Zero};
|
||||
|
||||
#[pyclass(module = false, name = "slice", unhashable = true, traverse)]
|
||||
#[pyclass(module = false, name = "slice", unhashable = true, traverse = "manual")]
|
||||
#[derive(Debug)]
|
||||
pub struct PySlice {
|
||||
pub start: Option<PyObjectRef>,
|
||||
@@ -23,11 +25,63 @@ pub struct PySlice {
|
||||
pub step: Option<PyObjectRef>,
|
||||
}
|
||||
|
||||
// SAFETY: Traverse properly visits all owned PyObjectRefs
|
||||
unsafe impl crate::object::Traverse for PySlice {
|
||||
fn traverse(&self, traverse_fn: &mut crate::object::TraverseFn<'_>) {
|
||||
self.start.traverse(traverse_fn);
|
||||
self.stop.traverse(traverse_fn);
|
||||
self.step.traverse(traverse_fn);
|
||||
}
|
||||
|
||||
fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
|
||||
if let Some(start) = self.start.take() {
|
||||
out.push(start);
|
||||
}
|
||||
// stop is not Option, so it will be freed when payload is dropped
|
||||
// (via drop_in_place on freelist pop, or Box::from_raw on dealloc)
|
||||
if let Some(step) = self.step.take() {
|
||||
out.push(step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SLICE_FREELIST: Cell<crate::object::FreeList<PySlice>> = const { Cell::new(crate::object::FreeList::new()) };
|
||||
}
|
||||
|
||||
impl PyPayload for PySlice {
|
||||
const MAX_FREELIST: usize = 1;
|
||||
const HAS_FREELIST: bool = true;
|
||||
|
||||
#[inline]
|
||||
fn class(ctx: &Context) -> &'static Py<PyType> {
|
||||
ctx.types.slice_type
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
|
||||
#[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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(with(Comparable, Representable, Hashable))]
|
||||
|
||||
@@ -53,6 +53,9 @@ unsafe impl Traverse for PyTuple {
|
||||
}
|
||||
}
|
||||
|
||||
// No freelist for PyTuple: structseq types (stat_result, struct_time, etc.)
|
||||
// are static subtypes sharing the same Rust payload, making type-safe reuse
|
||||
// impractical without a type-pointer comparison at push time.
|
||||
impl PyPayload for PyTuple {
|
||||
#[inline]
|
||||
fn class(ctx: &Context) -> &'static Py<PyType> {
|
||||
|
||||
@@ -171,13 +171,19 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
|
||||
// Untrack from GC BEFORE deallocation.
|
||||
if obj_ref.is_gc_tracked() {
|
||||
let ptr = unsafe { NonNull::new_unchecked(obj) };
|
||||
rustpython_common::refcount::try_defer_drop(move || {
|
||||
// untrack_object only removes the pointer address from a HashSet.
|
||||
// It does NOT dereference the pointer, so it's safe even after deallocation.
|
||||
unsafe {
|
||||
crate::gc_state::gc_state().untrack_object(ptr);
|
||||
}
|
||||
});
|
||||
if T::HAS_FREELIST {
|
||||
// Freelist types must untrack immediately to avoid race conditions:
|
||||
// a deferred untrack could remove a re-tracked entry after reuse.
|
||||
unsafe { crate::gc_state::gc_state().untrack_object(ptr) };
|
||||
} else {
|
||||
rustpython_common::refcount::try_defer_drop(move || {
|
||||
// untrack_object only removes the pointer address from a HashSet.
|
||||
// It does NOT dereference the pointer, so it's safe even after deallocation.
|
||||
unsafe {
|
||||
crate::gc_state::gc_state().untrack_object(ptr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract child references before deallocation to break circular refs (tp_clear)
|
||||
@@ -186,8 +192,17 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
|
||||
unsafe { clear_fn(obj, &mut edges) };
|
||||
}
|
||||
|
||||
// Deallocate the object memory
|
||||
drop(unsafe { Box::from_raw(obj as *mut PyInner<T>) });
|
||||
// Try to store in freelist for reuse; otherwise deallocate.
|
||||
// Only exact types (not heaptype subclasses) go into the freelist,
|
||||
// because the pop site assumes the cached typ matches the base type.
|
||||
let pushed = if T::HAS_FREELIST && obj_ref.class().heaptype_ext.is_none() {
|
||||
unsafe { T::freelist_push(obj) }
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !pushed {
|
||||
drop(unsafe { Box::from_raw(obj as *mut PyInner<T>) });
|
||||
}
|
||||
|
||||
// Drop child references - may trigger recursive destruction.
|
||||
// The object is already deallocated, so circular refs are broken.
|
||||
@@ -807,6 +822,52 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-local freelist storage that properly deallocates cached objects
|
||||
/// on thread teardown.
|
||||
///
|
||||
/// Wraps a `Vec<*mut PyObject>` and implements `Drop` to convert each
|
||||
/// raw pointer back into `Box<PyInner<T>>` for proper deallocation.
|
||||
pub(crate) struct FreeList<T: PyPayload> {
|
||||
items: Vec<*mut PyObject>,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: PyPayload> FreeList<T> {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyPayload> Default for FreeList<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyPayload> Drop for FreeList<T> {
|
||||
fn drop(&mut self) {
|
||||
for ptr in self.items.drain(..) {
|
||||
drop(unsafe { Box::from_raw(ptr as *mut PyInner<T>) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyPayload> core::ops::Deref for FreeList<T> {
|
||||
type Target = Vec<*mut PyObject>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyPayload> core::ops::DerefMut for FreeList<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.items
|
||||
}
|
||||
}
|
||||
|
||||
/// The `PyObjectRef` is one of the most used types. It is a reference to a
|
||||
/// python object. A single python object can have multiple references, and
|
||||
/// this reference counting is accounted for by this type. Use the `.clone()`
|
||||
@@ -1720,8 +1781,31 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> {
|
||||
pub fn new_ref(payload: T, typ: crate::builtins::PyTypeRef, dict: Option<PyDictRef>) -> Self {
|
||||
let has_dict = dict.is_some();
|
||||
let is_heaptype = typ.heaptype_ext.is_some();
|
||||
let inner = Box::into_raw(PyInner::new(payload, typ, dict));
|
||||
let ptr = unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) };
|
||||
|
||||
// Try to reuse from freelist (exact type only, no dict, no heaptype)
|
||||
let cached = if !has_dict && !is_heaptype {
|
||||
unsafe { T::freelist_pop() }
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ptr = if let Some(cached) = cached {
|
||||
let inner = cached.as_ptr() as *mut PyInner<T>;
|
||||
unsafe {
|
||||
core::ptr::write(&mut (*inner).ref_count, RefCount::new());
|
||||
(*inner).gc_bits.store(0, Ordering::Relaxed);
|
||||
core::ptr::drop_in_place(&mut (*inner).payload);
|
||||
core::ptr::write(&mut (*inner).payload, payload);
|
||||
// typ, vtable, slots are preserved; dict is None, weak_list was
|
||||
// cleared by drop_slow_inner before freelist push
|
||||
}
|
||||
// Drop the caller's typ since the cached object already holds one
|
||||
drop(typ);
|
||||
unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) }
|
||||
} else {
|
||||
let inner = Box::into_raw(PyInner::new(payload, typ, dict));
|
||||
unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) }
|
||||
};
|
||||
|
||||
// Track object if:
|
||||
// - HAS_TRAVERSE is true (Rust payload implements Traverse), OR
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
types::PyTypeFlags,
|
||||
vm::{Context, VirtualMachine},
|
||||
};
|
||||
use core::ptr::NonNull;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "threading")] {
|
||||
@@ -46,6 +47,38 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static {
|
||||
|
||||
fn class(ctx: &Context) -> &'static Py<PyType>;
|
||||
|
||||
/// Whether this type has a freelist. Types with freelists require
|
||||
/// immediate (non-deferred) GC untracking during dealloc to prevent
|
||||
/// race conditions when the object is reused.
|
||||
const HAS_FREELIST: bool = false;
|
||||
|
||||
/// Maximum number of objects to keep in the freelist.
|
||||
const MAX_FREELIST: 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).
|
||||
///
|
||||
/// # Safety
|
||||
/// `obj` must be a valid pointer to a `PyInner<Self>` with refcount 0,
|
||||
/// after `drop_slow_inner` and `tp_clear` have already run.
|
||||
#[inline]
|
||||
unsafe fn freelist_push(_obj: *mut PyObject) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Try to pop a pre-allocated object from this type's freelist.
|
||||
/// The returned pointer still has the old payload; the caller must
|
||||
/// reinitialize `ref_count`, `gc_bits`, and `payload`.
|
||||
///
|
||||
/// # Safety
|
||||
/// The returned pointer (if Some) must point to a valid `PyInner<Self>`
|
||||
/// whose payload is still initialized from a previous allocation. The caller
|
||||
/// will drop and overwrite `payload` before reuse.
|
||||
#[inline]
|
||||
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, vm: &VirtualMachine) -> PyObjectRef
|
||||
where
|
||||
|
||||
Reference in new issue
Block a user