Summary
vm.new_memory_error("") is how a failed allocation is reported, and raising it allocates four times. Under real memory pressure those allocations abort the process, which is the one situation the exception exists for.
Most of those call sites want something narrower: "an allocation just failed, say so". That deserves its own vm.no_memory_error(), taking no message, on a path that does not allocate — PyErr_NoMemory(), whose whole point is that it hands back a preallocated MemoryError rather than building one.
Details
vm.new_memory_error(msg) (crates/vm/src/vm/vm_new.rs:1019, through define_exception_fn!) goes
new_memory_error(msg)
-> new_exception_msg(type, msg) vm_new.rs:407
-> ctx.new_str(msg) context.rs:482 allocates a PyStr
-> vec![...] allocates a Vec
-> new_exception(type, args) vm_new.rs:344
-> PyBaseException::new(args, vm) allocates the args tuple
-> into_ref_with_type_lazy_dict(..) allocates the object
Every one of those object allocations lands on crates/vm/src/object/core.rs:1193, which calls alloc::alloc::handle_alloc_error on failure. With panic = "abort" (Cargo.toml:125) that is the process, not an exception.
The empty message is not free either. ctx.new_str("") takes the s.into_ref(self) branch: the latin1 singleton cache it checks first only covers single characters, so an empty message allocates a fresh PyStr on every raise, even though ctx.empty_str is sitting right there already interned (context.rs:41, context.rs:362).
What the call sites look like
28 calls to new_memory_error today; 20 of them pass "":
| argument |
count |
"" |
20 |
"source location is too large" |
3 |
"failed to query cert store" |
1 |
"failed to get cert store" |
1 |
"failed to allocate BIO" |
1 |
| (non-literal) |
2 |
The "" ones are the PyErr_NoMemory() shape — a try_reserve returned Err, or a foreign allocator returned null — and they read better as vm.no_memory_error() than as an error type plus an empty string that then has to be allocated.
The ones carrying a message are a separate question and can stay as they are; a few of them are arguably not MemoryError at all (nothing was allocated, a size was refused), but that is worth deciding case by case rather than in bulk.
What it would take
CPython keeps preallocated MemoryError instances so the raise itself never allocates, refilling the freelist when memory is available again and falling back to a statically allocated one when it is not. RustPython has the pieces to do the same: ctx.empty_str and ctx.empty_tuple already exist, and a preallocated instance could live next to them in Context.
The hazards are the ones CPython also has to handle, and are the actual design work here:
- a shared instance accumulates state across raises —
__traceback__, __context__, __cause__ — so it has to be reset, or handed out from a pool and returned
- reference cycles through a retained traceback
- thread safety, since the instance would be shared across interpreter threads
Not urgent — nothing here is a live crash — but the current shape means the OOM path is the least reliable path in the interpreter, which is backwards.
— reported by Claude
Summary
vm.new_memory_error("")is how a failed allocation is reported, and raising it allocates four times. Under real memory pressure those allocations abort the process, which is the one situation the exception exists for.Most of those call sites want something narrower: "an allocation just failed, say so". That deserves its own
vm.no_memory_error(), taking no message, on a path that does not allocate —PyErr_NoMemory(), whose whole point is that it hands back a preallocatedMemoryErrorrather than building one.Details
vm.new_memory_error(msg)(crates/vm/src/vm/vm_new.rs:1019, throughdefine_exception_fn!) goesEvery one of those object allocations lands on
crates/vm/src/object/core.rs:1193, which callsalloc::alloc::handle_alloc_erroron failure. Withpanic = "abort"(Cargo.toml:125) that is the process, not an exception.The empty message is not free either.
ctx.new_str("")takes thes.into_ref(self)branch: the latin1 singleton cache it checks first only covers single characters, so an empty message allocates a freshPyStron every raise, even thoughctx.empty_stris sitting right there already interned (context.rs:41,context.rs:362).What the call sites look like
28 calls to
new_memory_errortoday; 20 of them pass"":"""source location is too large""failed to query cert store""failed to get cert store""failed to allocate BIO"The
""ones are thePyErr_NoMemory()shape — atry_reservereturnedErr, or a foreign allocator returned null — and they read better asvm.no_memory_error()than as an error type plus an empty string that then has to be allocated.The ones carrying a message are a separate question and can stay as they are; a few of them are arguably not
MemoryErrorat all (nothing was allocated, a size was refused), but that is worth deciding case by case rather than in bulk.What it would take
CPython keeps preallocated
MemoryErrorinstances so the raise itself never allocates, refilling the freelist when memory is available again and falling back to a statically allocated one when it is not. RustPython has the pieces to do the same:ctx.empty_strandctx.empty_tuplealready exist, and a preallocated instance could live next to them inContext.The hazards are the ones CPython also has to handle, and are the actual design work here:
__traceback__,__context__,__cause__— so it has to be reset, or handed out from a pool and returnedNot urgent — nothing here is a live crash — but the current shape means the OOM path is the least reliable path in the interpreter, which is backwards.
— reported by Claude