Skip to content

Commit 33f54e0

Browse files
committed
Add per-size tuple freelist (20 buckets × 2000 each)
Implement PyTuple freelist matching tuples[PyTuple_MAXSAVESIZE]: - TupleFreeList with 20 per-size buckets (sizes 1..=20, 2000 capacity each) - freelist_push reads element count and stores in buckets[len-1] - freelist_pop takes &Self payload to select bucket by size - Add pyinner_layout<T>() helper for custom freelist Drop impls - Update freelist_pop trait signature to accept &Self across all types
1 parent 3b91466 commit 33f54e0

File tree

10 files changed

+103
-12
lines changed

10 files changed

+103
-12
lines changed

crates/vm/src/builtins/complex.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl PyPayload for PyComplex {
5858
}
5959

6060
#[inline]
61-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
61+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
6262
COMPLEX_FREELIST
6363
.try_with(|fl| {
6464
let mut list = fl.take();

crates/vm/src/builtins/dict.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ impl PyPayload for PyDict {
9393
}
9494

9595
#[inline]
96-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
96+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
9797
DICT_FREELIST
9898
.try_with(|fl| {
9999
let mut list = fl.take();

crates/vm/src/builtins/float.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ impl PyPayload for PyFloat {
6565
}
6666

6767
#[inline]
68-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
68+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
6969
FLOAT_FREELIST
7070
.try_with(|fl| {
7171
let mut list = fl.take();

crates/vm/src/builtins/int.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl PyPayload for PyInt {
8686
}
8787

8888
#[inline]
89-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
89+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
9090
INT_FREELIST
9191
.try_with(|fl| {
9292
let mut list = fl.take();

crates/vm/src/builtins/list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl PyPayload for PyList {
105105
}
106106

107107
#[inline]
108-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
108+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
109109
LIST_FREELIST
110110
.try_with(|fl| {
111111
let mut list = fl.take();

crates/vm/src/builtins/range.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ impl PyPayload for PyRange {
101101
}
102102

103103
#[inline]
104-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
104+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
105105
RANGE_FREELIST
106106
.try_with(|fl| {
107107
let mut list = fl.take();

crates/vm/src/builtins/slice.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ impl PyPayload for PySlice {
7676
}
7777

7878
#[inline]
79-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
79+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
8080
SLICE_FREELIST
8181
.try_with(|fl| {
8282
let mut list = fl.take();

crates/vm/src/builtins/tuple.rs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use crate::{
2727
vm::VirtualMachine,
2828
};
2929
use alloc::fmt;
30+
use core::cell::Cell;
31+
use core::ptr::NonNull;
3032

3133
#[pyclass(module = false, name = "tuple", traverse = "manual")]
3234
pub struct PyTuple<R = PyObjectRef> {
@@ -53,14 +55,98 @@ unsafe impl Traverse for PyTuple {
5355
}
5456
}
5557

56-
// No freelist for PyTuple: structseq types (stat_result, struct_time, etc.)
57-
// are static subtypes sharing the same Rust payload, making type-safe reuse
58-
// impractical without a type-pointer comparison at push time.
58+
// spell-checker:ignore MAXFREELIST MAXSAVESIZE
59+
/// Per-size freelist storage for tuples, matching tuples[PyTuple_MAXSAVESIZE].
60+
/// Each bucket caches tuples of a specific element count (index = len - 1).
61+
struct TupleFreeList {
62+
buckets: [Vec<*mut PyObject>; Self::MAXSAVESIZE],
63+
}
64+
65+
impl TupleFreeList {
66+
/// Largest tuple size to cache on the freelist (sizes 1..=20).
67+
const MAXSAVESIZE: usize = 20;
68+
const fn new() -> Self {
69+
Self {
70+
buckets: [const { Vec::new() }; Self::MAXSAVESIZE],
71+
}
72+
}
73+
}
74+
75+
impl Default for TupleFreeList {
76+
fn default() -> Self {
77+
Self::new()
78+
}
79+
}
80+
81+
impl Drop for TupleFreeList {
82+
fn drop(&mut self) {
83+
// Same safety pattern as FreeList<T>::drop — free raw allocation
84+
// without running payload destructors to avoid TLS-after-destruction panics.
85+
let layout = crate::object::pyinner_layout::<PyTuple>();
86+
for bucket in &mut self.buckets {
87+
for ptr in bucket.drain(..) {
88+
unsafe {
89+
alloc::alloc::dealloc(ptr as *mut u8, layout);
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
thread_local! {
97+
static TUPLE_FREELIST: Cell<TupleFreeList> = const { Cell::new(TupleFreeList::new()) };
98+
}
99+
59100
impl PyPayload for PyTuple {
101+
const MAX_FREELIST: usize = 2000;
102+
const HAS_FREELIST: bool = true;
103+
60104
#[inline]
61105
fn class(ctx: &Context) -> &'static Py<PyType> {
62106
ctx.types.tuple_type
63107
}
108+
109+
#[inline]
110+
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
111+
let py_tuple = unsafe { &*(obj as *const crate::Py<PyTuple>) };
112+
let len = py_tuple.elements.len();
113+
if len == 0 || len > TupleFreeList::MAXSAVESIZE {
114+
return false;
115+
}
116+
TUPLE_FREELIST
117+
.try_with(|fl| {
118+
let mut list = fl.take();
119+
let bucket = &mut list.buckets[len - 1];
120+
let stored = if bucket.len() < Self::MAX_FREELIST {
121+
bucket.push(obj);
122+
true
123+
} else {
124+
false
125+
};
126+
fl.set(list);
127+
stored
128+
})
129+
.unwrap_or(false)
130+
}
131+
132+
#[inline]
133+
unsafe fn freelist_pop(payload: &Self) -> Option<NonNull<PyObject>> {
134+
let len = payload.elements.len();
135+
if len == 0 || len > TupleFreeList::MAXSAVESIZE {
136+
return None;
137+
}
138+
TUPLE_FREELIST
139+
.try_with(|fl| {
140+
let mut list = fl.take();
141+
let result = list.buckets[len - 1]
142+
.pop()
143+
.map(|p| unsafe { NonNull::new_unchecked(p) });
144+
fl.set(list);
145+
result
146+
})
147+
.ok()
148+
.flatten()
149+
}
64150
}
65151

66152
pub trait IntoPyTuple {

crates/vm/src/object/core.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,11 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
860860
}
861861
}
862862

863+
/// Returns the allocation layout for `PyInner<T>`, for use in freelist Drop impls.
864+
pub(crate) const fn pyinner_layout<T: PyPayload>() -> core::alloc::Layout {
865+
core::alloc::Layout::new::<PyInner<T>>()
866+
}
867+
863868
/// Thread-local freelist storage for reusing object allocations.
864869
///
865870
/// Wraps a `Vec<*mut PyObject>`. On thread teardown, `Drop` frees raw
@@ -1883,7 +1888,7 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> {
18831888

18841889
// Try to reuse from freelist (exact type only, no dict, no heaptype)
18851890
let cached = if !has_dict && !is_heaptype {
1886-
unsafe { T::freelist_pop() }
1891+
unsafe { T::freelist_pop(&payload) }
18871892
} else {
18881893
None
18891894
};

crates/vm/src/object/payload.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static {
7575
/// whose payload is still initialized from a previous allocation. The caller
7676
/// will drop and overwrite `payload` before reuse.
7777
#[inline]
78-
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
78+
unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
7979
None
8080
}
8181

0 commit comments

Comments
 (0)