@@ -188,27 +188,32 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) { |
188 | 188 | ); |
189 | 189 | } |
190 | 190 |
|
191 | | - // Extract child references before deallocation to break circular refs (tp_clear) |
| 191 | + // Try to store in freelist for reuse BEFORE tp_clear, so that |
| 192 | + // size-based freelists (e.g. PyTuple) can read the payload directly. |
| 193 | + // Only exact base types (not heaptype or structseq subtypes) go into the freelist. |
| 194 | + let typ = obj_ref.class(); |
| 195 | + let pushed = if T::HAS_FREELIST |
| 196 | + && typ.heaptype_ext.is_none() |
| 197 | + && core::ptr::eq(typ, T::class(crate::vm::Context::genesis())) |
| 198 | + { |
| 199 | + unsafe { T::freelist_push(obj) } |
| 200 | + } else { |
| 201 | + false |
| 202 | + }; |
| 203 | + |
| 204 | + // Extract child references to break circular refs (tp_clear). |
| 205 | + // This runs regardless of freelist push — the object's children must be released. |
192 | 206 | let mut edges = Vec::new(); |
193 | 207 | if let Some(clear_fn) = vtable.clear { |
194 | 208 | unsafe { clear_fn(obj, &mut edges) }; |
195 | 209 | } |
196 | 210 |
|
197 | | - // Try to store in freelist for reuse; otherwise deallocate. |
198 | | - // Only exact types (not heaptype subclasses) go into the freelist, |
199 | | - // because the pop site assumes the cached typ matches the base type. |
200 | | - let pushed = if T::HAS_FREELIST && obj_ref.class().heaptype_ext.is_none() { |
201 | | - unsafe { T::freelist_push(obj) } |
202 | | - } else { |
203 | | - false |
204 | | - }; |
205 | 211 | if !pushed { |
206 | 212 | // Deallocate the object memory (handles ObjExt prefix if present) |
207 | 213 | unsafe { PyInner::dealloc(obj as *mut PyInner<T>) }; |
208 | 214 | } |
209 | 215 |
|
210 | 216 | // Drop child references - may trigger recursive destruction. |
211 | | - // The object is already deallocated, so circular refs are broken. |
212 | 217 | drop(edges); |
213 | 218 |
|
214 | 219 | // Trashcan: decrement depth and process deferred objects at outermost level |
@@ -1089,6 +1094,11 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> { |
1089 | 1094 | } |
1090 | 1095 | } |
1091 | 1096 |
|
| 1097 | +/// Returns the allocation layout for `PyInner<T>`, for use in freelist Drop impls. |
| 1098 | +pub(crate) const fn pyinner_layout<T: PyPayload>() -> core::alloc::Layout { |
| 1099 | + core::alloc::Layout::new::<PyInner<T>>() |
| 1100 | +} |
| 1101 | + |
1092 | 1102 | /// Thread-local freelist storage for reusing object allocations. |
1093 | 1103 | /// |
1094 | 1104 | /// Wraps a `Vec<*mut PyObject>`. On thread teardown, `Drop` frees raw |
@@ -2168,9 +2178,9 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> { |
2168 | 2178 | let has_dict = dict.is_some(); |
2169 | 2179 | let is_heaptype = typ.heaptype_ext.is_some(); |
2170 | 2180 |
|
2171 | | - // Try to reuse from freelist (exact type only, no dict, no heaptype) |
| 2181 | + // Try to reuse from freelist (no dict, no heaptype) |
2172 | 2182 | let cached = if !has_dict && !is_heaptype { |
2173 | | - unsafe { T::freelist_pop() } |
| 2183 | + unsafe { T::freelist_pop(&payload) } |
2174 | 2184 | } else { |
2175 | 2185 | None |
2176 | 2186 | }; |
@@ -2182,11 +2192,16 @@ impl<T: PyPayload + crate::object::MaybeTraverse + core::fmt::Debug> PyRef<T> { |
2182 | 2192 | (*inner).gc_bits.store(0, Ordering::Relaxed); |
2183 | 2193 | core::ptr::drop_in_place(&mut (*inner).payload); |
2184 | 2194 | core::ptr::write(&mut (*inner).payload, payload); |
2185 | | - // typ, vtable, slots are preserved; dict is None, weak_list was |
2186 | | - // cleared by drop_slow_inner before freelist push |
| 2195 | + // Freelist only stores exact base types (push-side filter), |
| 2196 | + // but subtypes sharing the same Rust payload (e.g. structseq) |
| 2197 | + // may pop entries. Update typ if it differs. |
| 2198 | + let cached_typ: *const Py<PyType> = &*(*inner).typ; |
| 2199 | + if core::ptr::eq(cached_typ, &*typ) { |
| 2200 | + drop(typ); |
| 2201 | + } else { |
| 2202 | + let _old = (*inner).typ.swap(typ); |
| 2203 | + } |
2187 | 2204 | } |
2188 | | - // Drop the caller's typ since the cached object already holds one |
2189 | | - drop(typ); |
2190 | 2205 | unsafe { NonNull::new_unchecked(inner.cast::<Py<T>>()) } |
2191 | 2206 | } else { |
2192 | 2207 | let inner = PyInner::new(payload, typ, dict); |
|