perf(vm): non-allocating finally-handler check on returns [2/4 of #39] - #41
Conversation
Every OpReturn/OpReturnUndefined called findAllExceptionHandlers, which walks the exception table and heap-allocates a []*ExceptionHandler whenever the return PC sits inside any try region - then the caller scanned the slice up to twice to find the first finally handler. Replace with findPendingHandler, a single table-order scan returning the same handler with no allocation, and apply it to the two generator ActionReturn chain sites (which additionally match iterator-cleanup handlers). Also reorder the top-level-script check in both return opcodes so the frameCount load short-circuits before the "<script>" string compare, which otherwise ran on every function return. Profile-driven: findAllExceptionHandlers was 1.8% flat in the recursion benchmark's CPU profile; the allocation only fired for returns inside try regions. Not separable from noise on the macro benchmarks (shared machine). TestScripts green; Test262 language suite (which covers try/finally semantics) clean on the series tip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It claimed to select what "the callers of findAllExceptionHandlers" used to pick, but the generator-resume path is still a caller and picks by different rules - last covering finally, and finally preferred over an earlier iterator-cleanup handler. As written the comment invited a future reader to convert that site too, which would be a real bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Regression on the arithmetic workloads. Measured on a dedicated
The shape is odd and might point at the cause: Floors from null controls in the same run: two commits compiling to byte-identical binaries measure up to 3.7% apart on |
|
Correcting my verdict here: the +29.3% is real, and it is not this change. The exposure runs the wrong way. What is left is code layout — this change re-encodes 35.6% of The non-allocating handler check still looks right on its merits, and it has no |
nooga
left a comment
There was a problem hiding this comment.
Verified locally: builds clean, merges without conflict alongside #40-43 (checked in sequence), go vet clean, full go test ./... (all packages) green, plus the vm-diff logic checked by hand (NaN comparison semantics, %-fast-path sign/overflow guards, findPendingHandler table-order equivalence to the old findAllExceptionHandlers scan, atomic.Bool race fix). See PR discussion for details.
Part 2 of the #39 split.
findAllExceptionHandlersallocates a[]*ExceptionHandleron everyOpReturn/OpReturnUndefined, where the common case is an empty exception table and the result is immediately reduced to "first handler that is a finally".findPendingHandlerfuses that filter into the scan and returns the handler directly.It's an exact refactor of the selection logic: same
frameCount/closureguards, same exception table, same iteration order, samepc >= TryStart && pc < TryEndpredicate. TheincludeIterCleanupflag covers the two call sites that also acceptIsIteratorCleanup.findAllExceptionHandlersis unchanged and still used by the unwinding and generator paths.Also reorders
vm.frameCount == 1 && function != nil && function.Name == "<script>"so the plain load short-circuits before the string compare, which otherwise ran on every function return.Where this sits in the #39 split
#39 bundled ~6 changes; per review it's split into four independent PRs, all branched off current
main:perf/vm-numeric-compare-rem%, drop unreachable per-op guardsperf/vm-finally-nonallocperf/vm-dispatch-deadcodeIsNaNcleanupperf/vm-array-index-accessor-atomicarrayIndexAccessorSeen→atomic.BoolThe other three pieces you flagged as unmentioned — the
IsObject()range check, theToInteger()fast path, and thearrayIndexAccessorSeenlatch itself — already landed onmainseparately, so they aren't repeated here. Together these four are the remainder of #39.The four merge onto
mainin sequence with no conflicts; the union passesTestScripts,pkg/vm,pkg/compiler, andgo test -race ./pkg/vm/.🤖 Generated with Claude Code