Skip to content

Ray tracer: parameters captured by copy, direct calls to methods no class overrides - #759

Merged
ASDAlexander77 merged 3 commits into
mainfrom
own-raytrace-perf
Oct 31, 2026
Merged

ASDAlexander77 merged 3 commits into
mainfrom
own-raytrace-perf

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Ray tracer performance (item 5): two compiler changes, and a Windows EH crash on main that the second one exposed.

Changes

  1. A closure copies a parameter nothing assigns (5b5d5cc). A captured parameter used to be moved into a heap cell shared by the frame and the closure's box, so getNaturalColor made six cells per call for addLight. A parameter that neither the body nor a closure in it assigns (++/-- now count as assignments) is captured by copy, as this and a const already were.
    • -mm=own: a closure that does not escape borrows the copy, generalizing spec 70's this rule (isBorrowableCapture: a parameter's read, or a copy the enclosing closure's box holds). Generator parameters, callbacks and Shared<T> handles stay in cells. An escaping closure over a parameter's copy is still refused with the old "its value is the caller's" message.
    • As a side effect, a nested non-escaping closure capturing an outer closure's const copy now compiles under own; it was an error on main.
  2. Windows EH: an inlined throw in a catch-all that ends in resume keeps its funclet bundle (2dab213). This was already broken on main: a using whose disposal is a direct call (a @final class) and throws crashed with --opt, because MLIR's inliner put the throw inside the catch funclet with no "funclet" bundle. 00using_dispose_throws (callerCatches, bothThrow) caught it once change 3 made dispose calls direct.
  3. A call through a vtable to a method no class overrides is a direct call (06328f4). After the module is generated, every class it made is known. A method that no class overrides, in a class that nothing outside the module can extend, gets ThisSymbolRef instead of ThisVirtualSymbolRef, as a @final receiver already does. "Nothing outside can extend" means the class is not exported, declared, imported or a C++ binding, and is not a base of such a class.

Ray tracer

Median of 7 runs, ms; every build prints the same checksum, and neither benchmark source changed.

Build main params by copy + direct calls vs Rust idiomatic (55)
gc 266 97 79 1.4x
rc 171 133 120 2.2x
none 157 96 79 1.4x
own 143 104 86 1.6x

Testing

  • ctest 6534/6534 (Windows Release).
  • Fuzz seeds 79000-80599 clean.
  • DefaultLib lib.ts compiles under own, rc, gc and none.
  • New tests:
    • own/own_param_capture_copies (all four models);
    • tests/00class_method_devirtualized: overrides declared after the call, two classes down, generic and function-local subclasses, and abstract methods;
    • class-method-devirtualized.cmake: an IR check of which calls are direct. It fails on main's compiler.
  • Not verified locally: Linux. The EH fix is Windows-only, but the same inlined-throw shape exists on Linux through linuxCleanupCatches. Read 00using_dispose_throws and 00class_method_devirtualized first on the Linux CI.

Known

  • Under own, an inner closure escaping from an outer closure is still an error, but its message changed from "it is captured from an enclosing function" to "borrows a field".
  • Left for a follow-up: Intersection | null (README variant B, about 16 ms). Its null test compares the type tag as a string, so fixing it means changing how null is stored in a union.

🤖 Generated with Claude Code

ASDAlexander77 and others added 3 commits October 31, 2026 01:32
…o a heap cell

A captured parameter was always moved into a heap cell shared by the frame and the
closure's box, so the ray tracer's getNaturalColor made six cells per call for its
addLight callback. A parameter that neither the body nor a closure in it assigns
(collectAssignedNames, which now also counts ++ and --) is captured by copy, as
`this` and a const already were.

-mm=own: a closure that does not escape borrows the copy, as spec 70 lets it borrow
`this` (OwnershipFacts isBorrowableCapture: a parameter's read, or a copy the
enclosing closure's box holds). Generator parameters, callbacks and Shared<T>
handles stay in cells. An escaping closure over a parameter's copy is refused with
the message its cell got.

Ray tracer, median ms (main -> this): gc 273 -> 97, none 158 -> 97, rc 171 -> 134,
own 143 -> 104. ctest 6527/6527; fuzz seeds 79000-79599 clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s its funclet bundle

A `using` whose disposal may throw is entered through a catch-all that ends the way a
cleanup does, in a `resume` (windowsCleanupCatches). With the disposal a direct call
(a @Final class), MLIR's inliner put its `throw` straight into that catch. The scan
leaves an inlined `_CxxThrowException` out of the calls it gives the funclet bundle,
expecting the catchret placed ahead of the region's end to take it out of the funclet;
a region ending in `resume` gets no catchret, so the throw stayed in the funclet with
no bundle, and the program crashed once optimized (00using_dispose_throws: callerCatches,
bothThrow, with --opt). Such a region now bundles them like its other calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the module is generated, every class it made is known (moduleClasses, imported
sources' too). A method no class in the module overrides, of a class nothing outside
the module can extend - not exported, declared, imported or a C++ binding, nor a base
of one - is reached by a vtable call alone, so devirtualizeClosedMethods makes its
ThisVirtualSymbolRef a ThisSymbolRef, as ClassMethodAccess does for a @Final receiver,
and LLVM can inline it.

Ray tracer, median ms (main -> parameters by copy -> this): gc 266 -> 97 -> 79,
none 157 -> 96 -> 79, rc 171 -> 133 -> 120, own 143 -> 104 -> 86; Rust idiomatic 55.
Test 00class_method_devirtualized (overrides declared after the call, two classes
down, generic and function-local subclasses, abstract) and an IR check of which calls
are direct. ctest 6534/6534; fuzz seeds 80000-80599 clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 0713e12 into main Oct 31, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-raytrace-perf branch October 31, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant