Skip to content

Commit a57152c

Browse files
committed
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.
1 parent 475167c commit a57152c

3 files changed

Lines changed: 46 additions & 1 deletion

File tree

crates/vm/src/builtins/dict.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ use crate::{
2626
vm::VirtualMachine,
2727
};
2828
use alloc::fmt;
29+
use core::cell::Cell;
30+
use core::ptr::NonNull;
2931
use rustpython_common::lock::PyMutex;
3032
use rustpython_common::wtf8::Wtf8Buf;
3133

@@ -60,11 +62,44 @@ impl fmt::Debug for PyDict {
6062
}
6163
}
6264

65+
const PYDICT_MAXFREELIST: usize = 80;
66+
67+
thread_local! {
68+
static DICT_FREELIST: Cell<Vec<*mut PyObject>> = const { Cell::new(Vec::new()) };
69+
}
70+
6371
impl PyPayload for PyDict {
72+
const HAS_FREELIST: bool = true;
73+
6474
#[inline]
6575
fn class(ctx: &Context) -> &'static Py<PyType> {
6676
ctx.types.dict_type
6777
}
78+
79+
#[inline]
80+
unsafe fn freelist_push(obj: *mut PyObject) -> bool {
81+
DICT_FREELIST.with(|fl| {
82+
let mut list = fl.take();
83+
let stored = if list.len() < PYDICT_MAXFREELIST {
84+
list.push(obj);
85+
true
86+
} else {
87+
false
88+
};
89+
fl.set(list);
90+
stored
91+
})
92+
}
93+
94+
#[inline]
95+
unsafe fn freelist_pop() -> Option<NonNull<PyObject>> {
96+
DICT_FREELIST.with(|fl| {
97+
let mut list = fl.take();
98+
let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
99+
fl.set(list);
100+
result
101+
})
102+
}
68103
}
69104

70105
impl PyDict {

crates/vm/src/builtins/tuple.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ unsafe impl Traverse for PyTuple {
5353
}
5454
}
5555

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.
5659
impl PyPayload for PyTuple {
5760
#[inline]
5861
fn class(ctx: &Context) -> &'static Py<PyType> {

crates/vm/src/object/core.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,14 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
193193
}
194194

195195
// Try to store in freelist for reuse; otherwise deallocate.
196-
if !unsafe { T::freelist_push(obj) } {
196+
// Only exact types (not heaptype subclasses) go into the freelist,
197+
// because the pop site assumes the cached typ matches the base type.
198+
let pushed = if T::HAS_FREELIST && obj_ref.class().heaptype_ext.is_none() {
199+
unsafe { T::freelist_push(obj) }
200+
} else {
201+
false
202+
};
203+
if !pushed {
197204
drop(unsafe { Box::from_raw(obj as *mut PyInner<T>) });
198205
}
199206

0 commit comments

Comments
 (0)