Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
f20fb34
time: use CLOCK_UPTIME_RAW for monotonic/perf_counter on Apple
moreal Sep 7, 2026
d427717
vm: derive f_lineno lazily instead of tracking prev_line per instruction
moreal Sep 7, 2026
e67cc57
vm: coalesce per-call thread-locals into one FRAME_SLOT_CACHE
moreal Sep 7, 2026
ae7ce96
vm: fold stop/finalizing checks into EVAL_BREAKER
moreal Sep 7, 2026
2f8eb35
gc: set the TRACKED bit with a plain store for freshly allocated objects
moreal Sep 7, 2026
364bb76
vm: skip thread exception publish when pushing/popping a None exc slot
moreal Sep 7, 2026
4a14b04
vm: allocation-free keyword-call fast path and super() MRO walk
moreal Sep 7, 2026
57c75d4
builtins: cheaper enumerate and zip iteration
moreal Sep 7, 2026
f5b2871
_asyncio: cache module objects and use native set/dict operations
moreal Sep 7, 2026
07c31da
perf: use mimalloc as global allocator for rustpython CLI
moreal Sep 7, 2026
6ed9a1a
vm: cap dict/set table shrink and compact holes on resize
moreal Sep 7, 2026
db9e765
_functools: add native _lru_cache_wrapper
moreal Sep 7, 2026
2a59cad
sre_engine: ASCII fast path for \d \s \w unicode category checks
moreal Sep 7, 2026
78cca65
vm: skip VM attach for no-op generator/coroutine __del__
moreal Sep 7, 2026
96018cf
_asyncio: skip full __import__ dispatch on module-cache hot path
moreal Sep 7, 2026
3f06556
vm: skip VM-attach section in with_vm when already on the current int…
moreal Sep 7, 2026
01619c2
vm: general sys.modules fast path for vm.import
moreal Sep 7, 2026
7573dde
_asyncio: bypass generic context.run dispatch in task_eager_start
moreal Sep 7, 2026
61f3684
vm: drop scopeguard::defer! from generator/coroutine resume hot path
moreal Sep 7, 2026
08d938d
vm: run generator bodies through the call trampoline
moreal Sep 7, 2026
580eab9
vm: run a `yield from` chain in one eval loop
moreal Sep 7, 2026
b3e46c1
vm: reset the oparg prefix when a signal unwinds a frame
moreal Sep 7, 2026
c41d05a
bytecode: fetch a code unit with one 16-bit acquire load
moreal Sep 7, 2026
4a008a2
vm: compute the cache-entry count after dispatch, not before
moreal Sep 7, 2026
8530edf
vm: move the eval-breaker body out of the dispatch loop
moreal Sep 7, 2026
654880e
vm: always inline the eval stack push/pop primitives
moreal Sep 7, 2026
66a38f2
vm: carry the instruction fetch state in loop locals
moreal Sep 7, 2026
1889376
bytecode: hoist the instruction array base out of the dispatch loop
moreal Sep 8, 2026
42aca22
common: an immortal bit in the refcount word
moreal Sep 8, 2026
fce1a8e
vm: make the singletons, small ints, interned strings and static type…
moreal Sep 8, 2026
bffef2e
codecs: fast-path utf-8 decode, skipping the codec registry round trip
moreal Sep 8, 2026
50934b9
codecs: fast-path utf-8/ascii/latin-1 encode and ascii/latin-1 decode
moreal Sep 8, 2026
ebf53f0
vm: make the runtime safe for borrowed stack entries
moreal Sep 8, 2026
54d7782
vm: read consumed stack entries in place instead of owning them
moreal Sep 8, 2026
0ad8ae3
vm: turn LOAD_FAST_BORROW into an actual borrow
moreal Sep 8, 2026
b95c00f
vm: exact-dict fast path for DICT_MERGE and CALL_FUNCTION_EX kwargs
moreal Sep 8, 2026
cbcc4b2
vm: skip the recursion guard hashing exact ints inside a tuple
moreal Sep 8, 2026
e308bde
vm: wire slot descriptors from __slots__ into C-level type slots
moreal Sep 8, 2026
ece684e
frame: stop allocating cold data to close a generator
moreal Sep 8, 2026
4508c40
function: bind exact args straight into a generator's frame
moreal Sep 8, 2026
00bb6a5
frame: drain a dead frame in place instead of moving it out
moreal Sep 8, 2026
be4d920
vm: don't raise a TypeError to ask an iterator for its length
moreal Sep 8, 2026
0c490ae
gc: track a generator and its frame in one pass
moreal Sep 8, 2026
13c0304
dict_inner: fast-path exact-int equality in key_eq
moreal Sep 8, 2026
0bf140b
vm: let the modulus type handle a three-argument pow
moreal Sep 8, 2026
000a972
vm: add a feature-gated opcode-pair histogram
moreal Sep 8, 2026
791e7a0
vm: let boolean producers run the branch that follows them
moreal Sep 8, 2026
5f48272
Adapt yield-from resume to optional generator frames
youknowone Sep 19, 2026
91928bb
Fix clippy and cr_origin after try_to_bool/f_lineno changes
youknowone Sep 19, 2026
7111aad
Convert fd filenames without the os ToPyObject impl
youknowone Sep 19, 2026
0abc5c9
Gate fd path helpers on the host_env feature
youknowone Sep 19, 2026
a8df5cd
Drop redundant pickle try_to_bool clones after rebase
youknowone Sep 19, 2026
11d6a20
Document why enumerate keeps a Counter beside BigInt
youknowone Sep 19, 2026
aa7a8b1
Move fd path helpers into ospath_fd under host_env
youknowone Sep 19, 2026
105ad02
Group threading eval-breaker bits in one module
youknowone Sep 19, 2026
9d8c910
Make InterpreterFrame construction unsafe
youknowone Sep 19, 2026
aa1298b
Move OsPathOrFd re-export to the top of ospath.rs
youknowone Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
vm: coalesce per-call thread-locals into one FRAME_SLOT_CACHE
set_current_frame ran on every frame enter/exit and touched three separate
thread_local! statics (each a _tlv_get_addr call on macOS). Merge them into
a single thread-local struct so the hot path does one TLS access.

Assisted-by: Claude Code:claude-fable-5-1
  • Loading branch information
moreal authored and youknowone committed Sep 19, 2026
commit e67cc57dbed0dfc8e4b1385f42e830b924a21239
135 changes: 78 additions & 57 deletions crates/vm/src/vm/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,32 @@ pub struct ThreadSlot {
#[cfg(feature = "threading")]
pub type CurrentFrameSlot = Arc<ThreadSlot>;

/// Coalesced per-thread frame-publishing state, touched on every
/// `enter_iframe`/`exit_iframe`. Bundling `current_frame` together with
/// the cached `top_frame`/`top_iframe` slot pointers means
/// `set_current_frame` needs a single `thread_local!.with()` call
/// (one `_tlv_get_addr` on macOS) instead of two or three separate
/// ones — each `.with()` on a distinct `thread_local!` is its own TLS
/// lookup even though the bodies are just a cached-pointer store.
struct FrameSlotCache {
/// Current top frame for signal-safe traceback walking.
/// Stores a `*const InterpreterFrame` as `usize`.
/// Read by faulthandler's signal handler to dump tracebacks without
/// accessing RefCell or locks. Uses AtomicUsize for async-signal-safety.
current_frame: AtomicUsize,
/// Cached pointer to this thread's `ThreadSlot::top_frame`, so the hot
/// push/pop path can publish the top frame with a single relaxed store and
/// no `CURRENT_THREAD_SLOT` RefCell borrow. Null until the slot is
/// initialized; the `Arc<ThreadSlot>` in `CURRENT_THREAD_SLOT` keeps the
/// pointee alive until `cleanup_current_thread_frames` clears this.
#[cfg(all(unix, feature = "threading"))]
top_frame: Cell<*const AtomicPtr<Py<FrameObject>>>,
/// Cached pointer to this thread's `ThreadSlot::top_iframe` for the hot
/// light-frame push/pop path. The slot's Arc keeps the pointee alive.
#[cfg(feature = "threading")]
top_iframe: Cell<*const AtomicUsize>,
}

thread_local! {
pub(super) static VM_STACK: RefCell<Vec<NonNull<VirtualMachine>>> = Vec::with_capacity(1).into();

Expand Down Expand Up @@ -108,27 +134,15 @@ thread_local! {
#[cfg(feature = "threading")]
static CURRENT_THREAD_SLOT: RefCell<Option<CurrentFrameSlot>> = const { RefCell::new(None) };

/// Current top frame for signal-safe traceback walking.
/// Stores a `*const InterpreterFrame` as `usize`.
/// Read by faulthandler's signal handler to dump tracebacks without
/// accessing RefCell or locks. Uses AtomicUsize for async-signal-safety.
pub(crate) static CURRENT_FRAME: AtomicUsize =
const { AtomicUsize::new(0) };

/// Cached pointer to this thread's `ThreadSlot::top_frame`, so the hot
/// push/pop path can publish the top frame with a single relaxed store and
/// no `CURRENT_THREAD_SLOT` RefCell borrow. Null until the slot is
/// initialized; the `Arc<ThreadSlot>` in `CURRENT_THREAD_SLOT` keeps the
/// pointee alive until `cleanup_current_thread_frames` clears this.
#[cfg(all(unix, feature = "threading"))]
static CURRENT_TOP_FRAME_SLOT: Cell<*const AtomicPtr<Py<FrameObject>>> =
const { Cell::new(core::ptr::null()) };

/// Cached pointer to this thread's `ThreadSlot::top_iframe` for the hot
/// light-frame push/pop path. The slot's Arc keeps the pointee alive.
#[cfg(feature = "threading")]
static CURRENT_TOP_IFRAME_SLOT: Cell<*const AtomicUsize> =
const { Cell::new(core::ptr::null()) };
pub(crate) static FRAME_SLOT_CACHE: FrameSlotCache = const {
FrameSlotCache {
current_frame: AtomicUsize::new(0),
#[cfg(all(unix, feature = "threading"))]
top_frame: Cell::new(core::ptr::null()),
#[cfg(feature = "threading")]
top_iframe: Cell::new(core::ptr::null()),
}
};

/// Cached pointer to this thread's `ThreadSlot::stop_requested`, for the
/// safepoint the dispatch loop takes once per instruction. Reading it
Expand Down Expand Up @@ -425,9 +439,11 @@ fn ensure_thread_slot(vm: &VirtualMachine) -> CurrentFrameSlot {
/// Make `slot` the current thread slot (and the cached top-frame pointer).
#[cfg(feature = "threading")]
fn set_current_thread_slot(slot: CurrentFrameSlot) {
#[cfg(unix)]
CURRENT_TOP_FRAME_SLOT.with(|c| c.set(&slot.top_frame));
CURRENT_TOP_IFRAME_SLOT.with(|c| c.set(&slot.top_iframe));
FRAME_SLOT_CACHE.with(|cache| {
#[cfg(unix)]
cache.top_frame.set(&slot.top_frame);
cache.top_iframe.set(&slot.top_iframe);
});
CURRENT_STOP_REQUESTED.with(|c| c.set(&slot.stop_requested));
CURRENT_THREAD_SLOT.with(|current| {
*current.borrow_mut() = Some(slot);
Expand Down Expand Up @@ -959,49 +975,51 @@ pub fn pop_thread_frame() {
#[must_use]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn set_current_frame(frame: *const InterpreterFrame) -> *const InterpreterFrame {
// Publish the top frame for cross-thread readers (faulthandler,
// sys._current_frames).
#[cfg(feature = "threading")]
{
CURRENT_TOP_IFRAME_SLOT.with(|slot| {
let slot = slot.get();
FRAME_SLOT_CACHE.with(|cache| {
// Publish the top frame for cross-thread readers (faulthandler,
// sys._current_frames).
#[cfg(feature = "threading")]
{
let slot = cache.top_iframe.get();
if !slot.is_null() {
unsafe { &*slot }.store(frame as usize, Ordering::Relaxed);
}
});
#[cfg(unix)]
CURRENT_TOP_FRAME_SLOT.with(|slot| {
let slot = slot.get();
if !slot.is_null() {
let fo_ptr = if frame.is_null() {
core::ptr::null_mut()
} else {
let frame_obj = unsafe { (*frame).frame_obj() };
frame_obj.map_or(core::ptr::null_mut(), |py| {
py as *const Py<FrameObject> as *mut Py<FrameObject>
})
};
unsafe { &*slot }.store(fo_ptr, Ordering::Relaxed);
#[cfg(unix)]
{
let slot = cache.top_frame.get();
if !slot.is_null() {
let fo_ptr = if frame.is_null() {
core::ptr::null_mut()
} else {
let frame_obj = unsafe { (*frame).frame_obj() };
frame_obj.map_or(core::ptr::null_mut(), |py| {
py as *const Py<FrameObject> as *mut Py<FrameObject>
})
};
unsafe { &*slot }.store(fo_ptr, Ordering::Relaxed);
}
}
});
}
CURRENT_FRAME.with(|c| c.swap(frame as usize, Ordering::Relaxed)) as *const InterpreterFrame
}
cache.current_frame.swap(frame as usize, Ordering::Relaxed)
}) as *const InterpreterFrame
}

/// Lightweight version that only writes to TLS CURRENT_FRAME, returning
/// Lightweight version that only writes to TLS `current_frame`, returning
/// the previous value. Does not update cross-thread top_frame (that's
/// updated by `set_current_frame` for FrameObject-based calls).
#[inline(always)]
#[must_use]
pub fn set_current_frame_nosave(frame: *const InterpreterFrame) -> *const InterpreterFrame {
CURRENT_FRAME.with(|c| c.swap(frame as usize, Ordering::Relaxed)) as *const InterpreterFrame
FRAME_SLOT_CACHE.with(|cache| cache.current_frame.swap(frame as usize, Ordering::Relaxed))
as *const InterpreterFrame
}

/// Get the current thread's top InterpreterFrame pointer.
/// Used by faulthandler's signal handler to start traceback walking.
#[must_use]
pub fn get_current_frame() -> *const InterpreterFrame {
CURRENT_FRAME.with(|c| c.load(Ordering::Relaxed)) as *const InterpreterFrame
FRAME_SLOT_CACHE.with(|cache| cache.current_frame.load(Ordering::Relaxed))
as *const InterpreterFrame
}

/// Update the current thread's exception slot atomically (no locks).
Expand Down Expand Up @@ -1082,10 +1100,12 @@ pub fn cleanup_current_thread_frames(vm: &VirtualMachine) {
};
if clear {
*s.borrow_mut() = None;
#[cfg(all(unix, feature = "threading"))]
CURRENT_TOP_FRAME_SLOT.with(|c| c.set(core::ptr::null()));
#[cfg(feature = "threading")]
CURRENT_TOP_IFRAME_SLOT.with(|c| c.set(core::ptr::null()));
FRAME_SLOT_CACHE.with(|cache| {
#[cfg(unix)]
cache.top_frame.set(core::ptr::null());
cache.top_iframe.set(core::ptr::null());
});
#[cfg(feature = "threading")]
CURRENT_STOP_REQUESTED.with(|c| c.set(core::ptr::null()));
}
Expand Down Expand Up @@ -1147,10 +1167,11 @@ pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) {
thread: std::thread::current(),
qsbr: crate::object::qsbr::QSBR.register(),
});
#[cfg(all(unix, feature = "threading"))]
CURRENT_TOP_FRAME_SLOT.with(|c| c.set(&new_slot.top_frame));
#[cfg(feature = "threading")]
CURRENT_TOP_IFRAME_SLOT.with(|c| c.set(&new_slot.top_iframe));
FRAME_SLOT_CACHE.with(|cache| {
#[cfg(unix)]
cache.top_frame.set(&new_slot.top_frame);
cache.top_iframe.set(&new_slot.top_iframe);
});
#[cfg(feature = "threading")]
CURRENT_STOP_REQUESTED.with(|c| c.set(&new_slot.stop_requested));

Expand Down