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: cap dict/set table shrink and compact holes on resize
asyncio's eager task path (and its per-task bookkeeping sets in general)
repeatedly adds and discards distinct task objects from a handful of
long-lived sets (_eager_tasks, _scheduled_tasks). Two dict_inner issues
turned that steady-state churn into the dominant cost of async_tree_eager:

1. resize() picked a target size of `used * 2` with no floor, so once a
   churny set settled around 1-2 live entries it shrank to a table that
   the very next insert would already overflow, forcing a resize on
   almost every single add. Clamping to the original minimum table size
   (8) restores the headroom dict/set start with.

2. `entries` only ever grows (holes from deletions are never reclaimed),
   so resize()'s rebuild pass scanned every entry ever inserted into the
   dict's lifetime, not just the live ones - a set with a stable working
   set size but heavy churn grew this scan, and the dict's memory, without
   bound. resize() now compacts holes out of `entries` first, matching
   CPython's compacting hash table and keeping the shape-stamp fast path
   (which already requires a hole-free layout) reachable more often.

Measured with a standalone async_tree micro-benchmark (6 levels x 6
branches, pyperformance's bm_async_tree "none"/"eager" shape) run inside
one process for 10 asyncio.run() iterations, comparing medians of
interleaved binary runs on an otherwise idle machine:

- eager: ~2.91s -> ~0.86s median (about 3.4x)
- none:  ~1.98s -> ~1.23s median (about 1.6x)

A single fresh-process run (no in-process accumulation) still improves
by roughly 15-20% for eager, since the resize-storm from point 1 hits
even a single run's worth of task churn.

test_asyncio (34 files, all passing), test_dict, test_set, test_weakref,
test_weakset, test_ordered_dict, test_gc, test_descr, test_class, and
`cargo test -p rustpython-vm -p rustpython-stdlib` all pass.

Assisted-by: Claude Code:claude-fable-5-1
  • Loading branch information
moreal authored and youknowone committed Sep 19, 2026
commit 6ed9a1a1b44e783fbfef0b9c2f3a9c9663c91fbb
56 changes: 37 additions & 19 deletions crates/vm/src/dict_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,34 +275,52 @@ impl GenIndexes {

impl<T> DictInner<T> {
fn resize(&mut self, new_size: usize) {
// Never shrink below the initial minimum table size (8). Without this
// floor, a dict/set that repeatedly adds and removes distinct keys
// (e.g. asyncio's per-task bookkeeping sets) settles at a tiny table
// after a resize and then re-triggers a resize on almost every single
// insert afterward, turning O(1) inserts into a resize storm.
let new_size = new_size.max(8);
let new_size = {
let mut i = 1;
while i < new_size {
i <<= 1;
}
i
};
// Compact away holes left by deleted entries. `entries` only ever
// grows via `unchecked_push`, so a dict/set that repeatedly inserts
// and removes distinct keys (e.g. asyncio's per-task bookkeeping
// sets) would otherwise keep every tombstoned entry forever, making
// this scan - and the dict's memory footprint - grow without bound
// relative to the live key count. Compacting here mirrors CPython's
// own compacting hash table and keeps entry positions dense, which
// is required for the hole-free shape-stamp fast path below.
if self.entries.len() != self.used {
let mut compacted = Vec::with_capacity(self.used);
compacted.extend(self.entries.drain(..).filter(Option::is_some));
self.entries = compacted;
}
self.indices = vec![IndexEntry::FREE; new_size];
let mask = (new_size - 1) as i64;
for (entry_idx, entry) in self.entries.iter_mut().enumerate() {
if let Some(entry) = entry {
let mut idxs = GenIndexes::new(entry.hash, mask);
loop {
let index_index = idxs.next();
unsafe {
// Safety: index is always valid here
// index_index is generated by idxs
// entry_idx is saved one
let idx = self.indices.get_unchecked_mut(index_index);
if *idx == IndexEntry::FREE {
*idx = IndexEntry::from_index_unchecked(entry_idx);
entry.index = index_index;
break;
}
// SAFETY: compaction above removed every hole, so every slot is
// occupied here.
let entry = entry.as_mut().unwrap();
let mut idxs = GenIndexes::new(entry.hash, mask);
loop {
let index_index = idxs.next();
unsafe {
// Safety: index is always valid here
// index_index is generated by idxs
// entry_idx is saved one
let idx = self.indices.get_unchecked_mut(index_index);
if *idx == IndexEntry::FREE {
*idx = IndexEntry::from_index_unchecked(entry_idx);
entry.index = index_index;
break;
}
}
} else {
//removed entry
}
}
self.filled = self.used;
Expand Down Expand Up @@ -336,9 +354,9 @@ impl<T> DictInner<T> {
self.resize(new_size)
}
}
// A resize keeps entry positions and rewrites only the index-index, so
// this stays the entry's index afterwards.
entry_index
// A resize may compact holes out of `entries`, shifting earlier
// positions, but the entry pushed above is always the last one.
self.entries.len() - 1
}

const fn size(&self) -> DictSize {
Expand Down