-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Remove Frame mutex and use DataStack bump allocator for LocalsPlus #7333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
youknowone
merged 14 commits into
RustPython:main
from
youknowone:worktree-frame-datastack
Mar 4, 2026
Merged
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f869c35
Remove PyMutex<FrameState> from Frame, use UnsafeCell fields directly
youknowone 85df41e
Add thread-local DataStack for bump-allocating frame data
youknowone e286597
Unify FastLocals and BoxVec stack into LocalsPlus
youknowone 9dbe95d
Use DataStack for LocalsPlus in non-generator function calls
youknowone a5981f0
Fix clippy: import Layout from core::alloc instead of alloc::alloc
youknowone 938fdf7
Fix vectorcall compatibility with LocalsPlus API
youknowone fa49f5d
Add datastack, nlocalsplus, ncells, tstate to cspell dictionary
youknowone 6a6e90d
Fix DataStack pop() for non-monotonic allocation addresses
youknowone 2cc6b58
Fix stale comments: release_datastack -> materialize_localsplus
youknowone f300ff3
Fix non-threading mode for parallel test execution
youknowone 927269c
Fix CallAllocAndEnterInit to use LocalsPlus stack API
youknowone e8d94bb
Use checked arithmetic in LocalsPlus and DataStack allocators
youknowone 16778fc
Address code review: checked arithmetic, threading feature deps, Send…
youknowone 22df0c6
Clean up comments: remove redundant/stale remarks, fix CPython refere…
youknowone File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add thread-local DataStack for bump-allocating frame data
Introduce DataStack with linked chunks (16KB initial, doubling) and push/pop bump allocation. Add datastack field to VirtualMachine. Not yet wired to frame creation.
- Loading branch information
commit 85df41e708fbcf514324849dca3e0e712f4ceaab
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| /// Thread data stack for interpreter frames. | ||
| /// | ||
| /// A linked list of chunks providing bump allocation for frame-local data | ||
| /// (localsplus arrays). Mirrors `_PyStackChunk` / `tstate->datastack_*` | ||
| /// from CPython. | ||
| /// | ||
| /// Normal function calls allocate from the data stack via `push()` (pointer | ||
| /// bump). Generators and coroutines use heap-allocated storage instead. | ||
| use alloc::alloc::{Layout, alloc, dealloc}; | ||
| use core::ptr; | ||
|
|
||
| /// Minimum chunk size in bytes (16 KB, matching CPython `_PY_DATA_STACK_CHUNK_SIZE`). | ||
| const MIN_CHUNK_SIZE: usize = 16 * 1024; | ||
|
|
||
| /// Extra headroom (in bytes) to avoid allocating a new chunk for the next | ||
| /// frame right after growing. | ||
| const MINIMUM_OVERHEAD: usize = 1000 * core::mem::size_of::<usize>(); | ||
|
|
||
| /// Alignment for all data stack allocations. | ||
| const ALIGN: usize = 16; | ||
|
|
||
| /// Header for a data stack chunk. The usable data region starts right after | ||
| /// this header (aligned to `ALIGN`). | ||
| #[repr(C)] | ||
| struct DataStackChunk { | ||
| /// Previous chunk in the linked list (NULL for the root chunk). | ||
| previous: *mut DataStackChunk, | ||
| /// Total allocation size in bytes (including this header). | ||
| size: usize, | ||
| /// Saved `top` offset when a newer chunk was pushed. Used to restore | ||
| /// `DataStack::top` when popping back to this chunk. | ||
| saved_top: usize, | ||
| } | ||
|
|
||
| impl DataStackChunk { | ||
| /// Pointer to the first usable byte after the header (aligned). | ||
| #[inline(always)] | ||
| fn data_start(&self) -> *mut u8 { | ||
| let header_end = (self as *const Self as usize) + core::mem::size_of::<Self>(); | ||
| let aligned = (header_end + ALIGN - 1) & !(ALIGN - 1); | ||
| aligned as *mut u8 | ||
| } | ||
|
|
||
| /// Pointer past the last usable byte. | ||
| #[inline(always)] | ||
| fn data_limit(&self) -> *mut u8 { | ||
| unsafe { (self as *const Self as *mut u8).add(self.size) } | ||
| } | ||
| } | ||
|
|
||
| /// Per-thread data stack for bump-allocating frame-local data. | ||
| pub struct DataStack { | ||
| /// Current chunk. | ||
| chunk: *mut DataStackChunk, | ||
| /// Current allocation position within the current chunk. | ||
| top: *mut u8, | ||
| /// End of usable space in the current chunk. | ||
| limit: *mut u8, | ||
| } | ||
|
|
||
| impl DataStack { | ||
| /// Create a new data stack with an initial root chunk. | ||
| pub fn new() -> Self { | ||
| let chunk = Self::alloc_chunk(MIN_CHUNK_SIZE, ptr::null_mut()); | ||
| let top = unsafe { (*chunk).data_start() }; | ||
| let limit = unsafe { (*chunk).data_limit() }; | ||
| // Skip one ALIGN-sized slot in the root chunk so that `pop()` never | ||
| // frees it (same trick as CPython's `push_chunk`). | ||
| let top = unsafe { top.add(ALIGN) }; | ||
| Self { chunk, top, limit } | ||
| } | ||
|
|
||
| /// Check if the current chunk has at least `size` bytes available. | ||
| #[inline(always)] | ||
| pub fn has_space(&self, size: usize) -> bool { | ||
| let aligned_size = (size + ALIGN - 1) & !(ALIGN - 1); | ||
| (self.limit as usize).saturating_sub(self.top as usize) >= aligned_size | ||
| } | ||
|
|
||
| /// Allocate `size` bytes from the data stack. | ||
| /// | ||
| /// Returns a pointer to the allocated region (aligned to `ALIGN`). | ||
| /// The caller must call `pop()` with the returned pointer when done | ||
| /// (LIFO order). | ||
| #[inline(always)] | ||
| pub fn push(&mut self, size: usize) -> *mut u8 { | ||
| let aligned_size = (size + ALIGN - 1) & !(ALIGN - 1); | ||
| unsafe { | ||
| if self.top.add(aligned_size) <= self.limit { | ||
| let ptr = self.top; | ||
| self.top = self.top.add(aligned_size); | ||
| ptr | ||
| } else { | ||
| self.push_slow(aligned_size) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Slow path: allocate a new chunk and push from it. | ||
| #[cold] | ||
| #[inline(never)] | ||
| fn push_slow(&mut self, aligned_size: usize) -> *mut u8 { | ||
| let mut chunk_size = MIN_CHUNK_SIZE; | ||
| let needed = aligned_size + MINIMUM_OVERHEAD + core::mem::size_of::<DataStackChunk>() + ALIGN; | ||
| while chunk_size < needed { | ||
| chunk_size *= 2; | ||
| } | ||
| // Save current position in old chunk. | ||
| unsafe { | ||
| (*self.chunk).saved_top = self.top as usize - self.chunk as usize; | ||
| } | ||
| let new_chunk = Self::alloc_chunk(chunk_size, self.chunk); | ||
| self.chunk = new_chunk; | ||
| let start = unsafe { (*new_chunk).data_start() }; | ||
| self.limit = unsafe { (*new_chunk).data_limit() }; | ||
| self.top = unsafe { start.add(aligned_size) }; | ||
| start | ||
| } | ||
|
|
||
| /// Pop a previous allocation. `base` must be the pointer returned by | ||
| /// `push()`. Calls must be in LIFO order. | ||
| /// | ||
| /// # Safety | ||
| /// `base` must be a valid pointer returned by `push()` on this data stack, | ||
| /// and all allocations made after it must already have been popped. | ||
| #[inline(always)] | ||
| pub unsafe fn pop(&mut self, base: *mut u8) { | ||
| debug_assert!(!base.is_null()); | ||
| let chunk_start = unsafe { (*self.chunk).data_start() }; | ||
| if base >= chunk_start { | ||
| // Common case: base is within the current chunk. | ||
| self.top = base; | ||
| } else { | ||
| // base is in a previous chunk — free the current chunk. | ||
| unsafe { self.pop_slow(base) }; | ||
| } | ||
| } | ||
|
|
||
| /// Slow path: pop back to a previous chunk. | ||
| #[cold] | ||
| #[inline(never)] | ||
| unsafe fn pop_slow(&mut self, base: *mut u8) { | ||
| let old_chunk = self.chunk; | ||
| let prev = unsafe { (*old_chunk).previous }; | ||
| debug_assert!( | ||
| !prev.is_null(), | ||
| "tried to pop past the root chunk" | ||
| ); | ||
| unsafe { Self::free_chunk(old_chunk) }; | ||
| self.chunk = prev; | ||
| self.top = base; | ||
| // If the base is not within this previous chunk either, we have | ||
| // a logic error (LIFO violation). | ||
| debug_assert!( | ||
| base >= unsafe { (*prev).data_start() } && base <= unsafe { (*prev).data_limit() }, | ||
| "pop base not in previous chunk (LIFO violation)" | ||
| ); | ||
| self.limit = unsafe { (*prev).data_limit() }; | ||
| } | ||
|
|
||
| /// Allocate a new chunk. | ||
| fn alloc_chunk(size: usize, previous: *mut DataStackChunk) -> *mut DataStackChunk { | ||
| let layout = Layout::from_size_align(size, ALIGN).expect("invalid chunk layout"); | ||
| let ptr = unsafe { alloc(layout) }; | ||
| if ptr.is_null() { | ||
| alloc::alloc::handle_alloc_error(layout); | ||
| } | ||
| let chunk = ptr as *mut DataStackChunk; | ||
| unsafe { | ||
| (*chunk).previous = previous; | ||
| (*chunk).size = size; | ||
| (*chunk).saved_top = 0; | ||
| } | ||
| chunk | ||
| } | ||
|
|
||
| /// Free a chunk. | ||
| unsafe fn free_chunk(chunk: *mut DataStackChunk) { | ||
| let size = unsafe { (*chunk).size }; | ||
| let layout = Layout::from_size_align(size, ALIGN).expect("invalid chunk layout"); | ||
| unsafe { dealloc(chunk as *mut u8, layout) }; | ||
| } | ||
| } | ||
|
|
||
| // SAFETY: DataStack is per-thread and not shared. The raw pointers | ||
| // it contains point to memory exclusively owned by this DataStack. | ||
| unsafe impl Send for DataStack {} | ||
|
|
||
| impl Default for DataStack { | ||
| fn default() -> Self { | ||
| Self::new() | ||
| } | ||
| } | ||
|
|
||
| impl Drop for DataStack { | ||
| fn drop(&mut self) { | ||
| let mut chunk = self.chunk; | ||
| while !chunk.is_null() { | ||
| let prev = unsafe { (*chunk).previous }; | ||
| unsafe { Self::free_chunk(chunk) }; | ||
| chunk = prev; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn basic_push_pop() { | ||
| let mut ds = DataStack::new(); | ||
| let p1 = ds.push(64); | ||
| assert!(!p1.is_null()); | ||
| let p2 = ds.push(128); | ||
| assert!(!p2.is_null()); | ||
| assert!(p2 > p1); | ||
| unsafe { | ||
| ds.pop(p2); | ||
| ds.pop(p1); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn cross_chunk_push_pop() { | ||
| let mut ds = DataStack::new(); | ||
| // Push enough to force a new chunk | ||
| let mut ptrs = Vec::new(); | ||
| for _ in 0..100 { | ||
| ptrs.push(ds.push(1024)); | ||
| } | ||
| // Pop all in reverse | ||
| for p in ptrs.into_iter().rev() { | ||
| unsafe { ds.pop(p) }; | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn alignment() { | ||
| let mut ds = DataStack::new(); | ||
| for size in [1, 7, 15, 16, 17, 31, 32, 33, 64, 100] { | ||
| let p = ds.push(size); | ||
| assert_eq!(p as usize % ALIGN, 0, "alignment violated for size {size}"); | ||
| unsafe { ds.pop(p) }; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.