Repository navigation
perf: make raising and catching exceptions cheaper - #88
Merged
Merged
Conversation
An exception leaving an activation that a traceback entry recorded lazily used to build the frame object at the pop, so a raise caught one frame up (`try: f() except E: pass`) paid for a frame object nobody read. The activation now departs instead: its shell, whose slots the pop already upgrades, stands for the frame object, which is built on first use (`tb_frame`, `f_back`, the collector's traversal) exactly as the pop would have left it, off the spine at its last instruction with its caller as `f_back`. The departed shell holds its caller's shell the way CPython's frame object holds `f_back`, so an unwound chain's locals now die inner frame first, as in CPython (they used to die outer frame first). The native AST builder leaves nodes whose fields go through a Python `setattr` to the Python builder, whose frame keeps the node alive above the setter's in the traceback.
The quiet loop looked a frame's handler up twice per catch (once to decide, once to jump) and its unwind once more after the caller proved there was none; `handle_exception_with` takes the answer. A raise of a builtin class walked the MRO for `BaseExceptionGroup` twice, now one memoised family bit; an `except` naming a superclass of the raised one trusts the class's exception flag before walking for `BaseException`; exception instances are built with their laid-out slots instead of replacing an empty table; and a departing activation decides in one pass of its pop, with its departure flag an atomic.
- A failing builtin constructor (`int("x")`) tried its pure build up to
eight times before the full call: `core_builtin_ctor` retried
`builtin_ctor_pure` after `leaf_type_call`, which ends in it, and
`core_native_ctor_kw` tried it again for builtin types that every
caller also hands to `core_builtin_ctor`. Each site now tries once.
- An exception's `args` tuple is built straight from the arguments, and
the memoised exception families are read in line.
- A traceback entry releases its chain without borrow guards and no
longer counts as a nesting container (it tears its chain down
iteratively, and what it holds nests on its own).
- `PUSH_EXC_INFO`/`POP_EXCEPT` in the core loop update the handled
exception stack under the raw-access contract of `exc_info_len`.
- A pop borrows the frame spine once, and a dying instance's class is
read without a guard when nothing else can borrow it.
`BaseException.__init__` copied its arguments into a vector and then into the `args` tuple, and walked the MRO by name for `StopIteration` on every call; the construction path's argument seeding collected into a vector the same way. Both now move the arguments straight into the tuple (`Object::tuple_from_exact`) and read the memoised exception families.
The catch path scanned the frame spine for the raising activation's shell under a borrow guard; only this thread mutates its spine and the scan runs no code, so it reads it as `exc_info_len` reads the handled exception stack. The handler also takes the exception instance from the record instead of cloning it and dropping the record's handle.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Makes raising, catching and releasing an exception cheaper.
tb_frame,f_back, the collector orgc.get_referentsreads it. A departed shell holds its caller's the way CPython's frame holdsf_back, so an unwound chain's locals now die innermost frame first, as in CPython (main freed them outermost first).BaseExceptionGroupmembership is a memoized family bit instead of two MRO walks, and a failing builtin constructor (int("x")) no longer retries its pure build eight times.argstuples without an intermediate vector; traceback entries are recorded and released without the spine's borrow guards.The free-order change made the native
_ast._builddisagree with the Python builder intests/regrtest/test_native_ast_build_cleanup.py. The test is unchanged; instead, the native builder now hands a node whose fields go through a Python-levelsetattrto the Python builder. Reviewers may want to look at that trade.Measurements
macOS, instructions per iteration,
mainto this branch (CPython 3.14 in parentheses):try/exceptof a raisedValueErrorKeyErrorcaughtValueError("x")built and freedint("x")Whole suite, instructions against
main:exceptions0.901,context_managers0.979, geomean 0.9915; no workload is worse by more than 0.5%.Validation
-D warningsis clean.test_exceptions,test_traceback,test_exception_group,test_except_star,test_sys_settrace,test_monitoring,test_contextlibandtest_raisepass with no unexpected results.What's left
A raise across a call is still about 2.1 times CPython's cost: the rest is the activation protocol's exceptional return (see RFC 0078 in #86).