perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS) #385
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!385
Loading…
Reference in a new issue
No description provided.
Delete branch "madebyjordan/Longshot:perf/split-giants"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
A player's first build spends most of its time compiling the lifted game code, and a few hundred huge lifted functions
(up to 6 MB of C each) cost far more than their size: clang's work on one function grows faster than linearly.
Compiling each of them as small pieces cuts the first build's compile by a fifth on a 6-core, 16 GB PC (22.4 -> 17.6
minutes), with the same game.
What changed
nx2/split_giants.py(new). For each lifted function of 256 KB of C or more,split_text()cuts the body atits top-level labels into pieces of about 8 KB (
plan()keeps short loops inside one piece). Each piece is astatic __attribute__((noinline)) uintptr_t s_<addr>_<n>(Ctx *c, uintptr_t k)that starts at the label numberedkand returns the next label's number, 0 for a guest return, or a tail-call number.
f_<addr>keeps its name and ABIand becomes the dispatch loop; guest tail calls stay
TAILCALL(musttail) from it, so the guest call depth neverchanges. This is exact because a lifted function keeps all guest state in the context and guest memory, and every C
local lives inside one instruction's block (
emit_functioninthird_party/a64-kit/lift/a64_lift.py): nothing islive across a top-level label.
items_of()and_local_gotos()leave a function whole when its text doesn't showthat: a top-level declaration (
A64_LOCAL_REGS), a label inside a block, a goto to a label outside its own statement,another shape of return or tail call, a literal.
nx2/build.py.split_giants_flags()adds-DLONGSHOT_SPLIT_GIANTS=<KB>_<hash of split_giants.py>to thelifted chunks' flags (not the hot pieces: their profile was counted on whole functions), so the object keys follow
the switch and the splitter.
compile_one()then compiles a split copy beside the object (<obj>.split.c, removedafter;
split_giants.compile_copy()). The lifted files, the lift and its key, the game's patches and every tool thatreads the lifted C are unchanged.
mem_need()counts the pieces as functions, so the memory gate sees a split chunk'sreal need.
tests/test_split_giants.py(new). It needs Clang (RUN_ALL_ARGS = ["--cc", "clang"]: run_all.py skips itwithout clang and fails it with
--strict; by hand, a missing compiler or one that isn't Clang fails it), as the buildcompiles lifted code with clang on every OS. Text checks, then three kinds of function compiled whole and split into one
program and run from the same 300 random states, compared: random functions in the lifter's shapes written by the test;
functions emitted by the real
a64_lift.Lifter.emit_function(withlift_fixesandpollinstalled) over a made-upimage: an entry with code below it, loops, B.cond, TBZ, CBNZ, BL, BLR, a tail call into another entry, and a setjmp
owner's call that sometimes longjmps back (host/hos_setjmp.c's assembly setjmp/longjmp), plus that function emitted with
A64_LOCAL_REGS/A64_LOCAL_FLAGS, which must be left whole; andbuild.py'scompile_onewith the split flag.split_giants_flags()): unset or any number turns it on,0off.docs/development.md, the switch in the build switches table.Flags and settings
LONGSHOT_SPLIT_GIANTS=<KB><KB>KB cut at its labels;0: as before0is the way backMeasurements
main:
cf605c85(2026-11-14)this PR: bold/split @ 012c615b
Rebased since onto main
0caf0618(now9f91cf6c), with the switch made on by default at 8: what the PR column measured withLONGSHOT_SPLIT_GIANTS=8is now the default build. The numbers still hold: #357 and #358, merged since, change the lift and the button prompts, notbuild.py's compile.Hardware: Ryzen 5 5600 (6 cores, 12 threads), 16 GB, RX 6700 XT, Crucial P3 NVMe (QLC), Windows 11 Home 26200
Scene: no game run:
nx2/build.py --standalone --jobs 4of one lift of the game's code (nightly-34's exefs, lifted bymain 514591a3's
nx2/lift.py; 75 lifted chunks, 1,381 MB of C), from an empty object cache each time (a first build'scompile), the launcher's toolchain (llvm-mingw, clang 23),
RX_RELEASE=1 A64_HOT_CHUNK=1, no hot list.Method: whole builds one at a time on an otherwise quiet PC, interleaved: this PR with the switch unset, PR, PR, main,
PR, main. (The first,
LONGSHOT_SPLIT_GIANTSunset, was off at the time; it is main's build.) Wall time of build.py; compiler-seconds summed from build.py's@@file <name> <seconds>lines; free RAM andthe compilers' summed working sets sampled every second. Medians.
How to reproduce: in a work folder with a lift (
OOT_WORK), delete<work>/objcacheand<work>/sa/build, thenpython -I -B nx2/build.py --standalone --jobs 4, with and withoutLONGSHOT_SPLIT_GIANTS=8.cf605c85LONGSHOT_SPLIT_GIANTS=8longshot.exe,longshot_vk.dllThe switch unset gives main's build: the build log's
object keys:line is the same (flags 4e8397c788,lifted code 410549c0cf) andsplit_giants_flags()adds nothing. That build ran once, first, at 1,634 s, and is left out of themedians. What says it was the PC and not the build, from its own log against main's two runs: 74 of the 75 lifted chunks
were slower (median x1.26, range 1.00-1.40), and so were all 12 files of the runtime and renderer that take over 5 s
(median x1.03); the extra time follows each file's compile time (r = 0.79), not its size (r = -0.19), so it was not a
per-file cost such as a first-read scan; and it came and went over the build (by quarter of the lifted compiles x1.34,
1.33, 1.02, 1.21). Free RAM was the same as in main's runs (median 5,227 MB against 5,167 and 5,125), and no other
heavy job ran (the PC's shared job lock). There is no per-process CPU record of the time, so what else used the CPU is
not known.
Memory, by what was measured: across the whole build (4 compilers) the compilers' summed peak working set fell
5,129.5 -> 4,840 MB (medians, -5.6%), and the least free RAM did not change in a way one can tell from 2-3 runs. The giant
chunks compiled one at a time: 2.62x faster in all (1,462 -> 559 s), the largest peak 2,263 -> 586 MB (25.9%). A single
2-6 MB function's own peak falls to about a fifth (1,220 -> 247 MB, 2,263 -> 428 MB); that holds for single giants, not
for whole chunks or the build.
Single compiles, quiet, one compiler (CPU s of clang, peak working set), the same lift:
cf605c85=8Piece size (three 2 MB functions at 4, 8 and 32 KB): all within 15% of each other, 8 KB best or tied. A split giant's
.textgrows 0.5-0.6%.Testing
Tested:
python -I -B tests/run_all.py --jobs 4, Windows 11: main @0caf0618485 passed, 6 failed, 70 skipped; this PR @9f91cf6c486 passed (test_split_giantsadded), the same 6 failed, 70 skipped. The 6 fail on main on this PC too:test_module_ids, test_pad_layout, test_oot_state_tap, test_portable_builder_home, test_portable_builder_startup,
test_tested_configurations.
tests/test_split_giants.pywith Clang 23 (llvm-mingw), each function run whole and split from 300 random states,comparing the context, guest memory, page marks, the calls out (each with a hash of the context), the setjmp resumes
and the interrupt polls: 4 written lifted-shaped functions (20-47 pieces), holding #38's fast-float region shape;
3 functions emitted by the real
emit_function(13, 23 and 29 pieces; 233, 361 and 351 of their states resumedthrough setjmp/longjmp); and a 406 KB chunk through
build.py'scompile_onewith the split flag (its object holdsthe pieces, the copy is removed): 0 differences in all 2,400 states.
A64_LOCAL_REGS/A64_LOCAL_FLAGSfunctions areleft whole. Through run_all.py: skipped without clang, failed with
--strict; by hand,--ccnaming a missingcompiler or one that isn't Clang fails. #38's real fast-float emitter is not on main, so its regions are covered
by the written shape only.
have no label to cut at; none is refused for a safety reason.
list, and f_81c1a240): 160 calls of each recorded in play (the context, and every guest page as the call first
touched it), then the lifted function and the split one run from each state: 320 states, 0 differences in the
context, the memory, the page marks, the calls out and the polls. Timed on those states (12 alternating calls each, 60 states per function, memory put back before each call): split /
whole, sum of mean times 0.979 (f_81b8c000) and 0.976 (f_81c1a240), of best times 0.975 and 0.969; a call takes
~2.5 us. Each crossing between pieces adds a call, a return and a dispatch, and can cost reloads; on these states
that was less than what the smaller functions gained.
=8, all 292 functions of 256 KB or more split), each in atest folder of nightly-34's game files, a scripted 300 s headless route (title, file select, the first area,
walking, menus), alternated twice: all 4 runs went the full 300 s into play (up to ~1,800 draws a frame), no crash.
Frame rates sat at the 60 fps cap: no speed claim from them.
all-thread samples: the same frame rates within noise, and the giants in no sample on either side. One split run
stopped at 43 s inside the development sampler's own stack walk (HOS_SAMPLE_ALL, #383), at the title,
before the route reaches the split functions.
Not tested: a game workload that spends real time in split functions, uncapped: the route's 2 ms samples never
landed in the two recorded giants, and its frame rate sat at the cap, so the in-game cost of the crossings is known only
from the replay timing above (two functions, recorded states); a scene where f_81b8c000 (#14 in the old sampled hot
list) is hot was not found. A build with a hot list (code_hot.c, -O2 + PGO, left unsplit on purpose; a function hot
in another scene but not on the list is split). Real states for the other 290 split functions (the same mechanism and
text checks, no recording). Linux builds (clang there too; the pieces use the GNU
case a ... brange andmusttail,which the lifted code already uses). PR #38's real emitter together with this (not on main).
Needs other testers:
LONGSHOT_SPLIT_GIANTS=0, expecting a shorter compileand the game unchanged (T-Q2FR)
Screenshots
Nothing on screen.
Risk and rollback
A lifted function compiled in pieces that behaved differently would show as a wrong game state; the checks above found
none, and any function whose text doesn't show the conditions stays whole.
LONGSHOT_SPLIT_GIANTS=0builds as before.On by default, the first build after this lands compiles every lifted chunk again (their flags change), as a
translator change does; a player whose lifted C was let go gets the "does not fit" path and one re-lift.
Reviewer notes
items_of(),_local_gotos()andsplit_function()innx2/split_giants.pyfirst: the refusals are the safety.{ if (A64_FF_ON(c)) { ... goto L_ff_<pc>; ... } <exact copy> L_ff_<pc>:; });_local_gotos()keeps such a statement whole in one piece, so the twocombine at the source level (the test holds that shape, written by hand: #38's emitter is not on main).
speed-ups are not necessarily additive.
.split.ccopy's path for a chunk with a giant.over a build.
Issues
See #362 (the build-speed findings) and #383 (the sampler crash seen while measuring)
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.tests/that reads changed source updated in this PR, not weakened.WIP: perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS)to perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS)Read head
ab831f76against main0caf06184: the complete four-file diff, description, empty comment and review lists, main's emitter and chunk writer, runtime dispatch and setjmp paths, and the related changes in #31 and #38; ran the new test with Clang 23.1.3 and GCC 16.2.1, plus nine existing compiler, memory-gate and cache checks. Clang reports 0 differences in all four sets of 300 states; GCC fails all four compiles on LLVM-only flags; the nine existing checks pass. No game, full build or timing run was made here.What's good
a64_lift.py:1029-1067can split only where no branch crosses, andnx2/chunks.py:394-424cuts between functions, not inside a giant loop or switch. The new transform addresses that remaining case without changing the saved lift, its patches or its address table.Must change
tests/test_split_giants.py:233-235unconditionally passes-mllvm -rotation-max-header-size=0, whilefind_cc, lines 200-212, acceptsCC=gcc,--cc gcc, and a GCC-only installation. Reproduced withpython3 -I -B tests/test_split_giants.py --cc gcc: all four compiles reportunrecognized command-line optionfor both options, thensplit_giants: FAILED (4). No whole-versus-split comparison runs on that path. Either support the selected compiler throughout the fixture, or require Clang explicitly with the existingRUN_ALL_ARGSconvention and remove the GCC fallback. That declaration also lets a strict suite detect a missing compiler instead of accepting the current success after all compiled comparisons were skipped. This is a test defect, not evidence that the split game misbehaves.What crosses a cut
nx2/split_giants.py:175-239passes the same context pointer to every piece, without copying it. Integer registers including SP and LR, vector registers, NZCV, FPCR/FPSR, exclusive-monitor state, thread pointers, PC, halt and poll state remain in that context. Guest memory remains shared. The normal emitter's C temporaries stay inside an instruction's balanced block, including instructions emitted over several lines.A64_LOCAL_REGSandA64_LOCAL_FLAGSinstead create function-scope declarations, whichitems_ofrefuses. The dead-flag analysis has already run on the original guest control flow, so a new piece call does not introduce guest ABI flag clobbering.The control paths are accounted for:
gotoadded bynx2/lift_fixes.pywhen an entry follows lower-address code.goto. A branch across pieces returns the destination label's token. The outer switch routes that token to its owning piece, whose switch enters the exact label. Sequential fallthrough returns the next piece's first label token. Every generated label token has a receiving case.TAILCALLin that wrapper, preserving the original signature and musttail behavior. Pieces do not change guest LR or guest SP just to cross a cut.A64_IJUMPbefore returning, including the existing longjmp hook. Ordinary calls resume at their original following statement.Functions remain whole when too small or without a usable cut, or when the parser sees recognized top-level declarations, nested guest labels, missing targets, nonlocal private labels, computed gotos, unsupported setjmp/return/tail-call forms, literals, multiline comments or unbalanced braces. These checks describe this emitter's text, not a general C parser.
What the equivalence evidence establishes
No specific guest input that changes the result was established in this review. Ordinary floating-point edge values are not newly approximated by this splitter: it retains the same arithmetic statements and helpers. The reported 320 states are 160 calls each of two functions, not coverage of the other 290 split functions or every edge through those two. An unvisited switch destination, exception path, or different emission mode could escape that sample. The replay method and results are reported, but its comparison code and coverage record are not included, so I could not inspect how external calls, concurrent memory changes or timing-dependent reads were handled.
The committed fixture provides useful comparisons of the whole context, memory, marks, call events and poll counts, but generates its own C shapes rather than using
emit_function. Its fast-float example is integer arithmetic with a private label; the real floating-point headers are empty stand-ins, and there is no setjmp case. Add made-up functions emitted through the actual emitter for non-first entry, setjmp resume, local-register/flag refusal, and the combined float-region path. Also exercisecompile_onewith the split flag: the current check callscompile_copyand the memory estimator directly, not that integration. These are coverage requests, not additional observed runtime defects.Measurements and runtime cost
The reported compile comparison has named commits, one PC, the same lift and settings, and at least two interleaved runs of each in its stated order. The medians are correct: 1,347 s to 1,057 s is 21.53% less wall time. The retained ranges are 1,339-1,355 s and 1,046-1,070 s, with spreads of 1.19% and 2.27% of their medians. Those numbers support a compile-stage saving for this Windows, four-worker, no-hot-list configuration, not a measured reduction in the complete first build on every PC. Keep the excluded 1,634 s baseline visible; the assertion that the PC caused it needs the retained load record. The measured source
012c615bis not available from the fetched refs or its public commit endpoint, so the current-head carry cannot be independently checked from that source."A fifth of the memory" applies to the two individual examples, not the whole build. Their peaks are 2,263 to 428 MB and 1,220 to 247 MB. Across the build, the median summed compiler peak is 5,129.5 to 4,840 MB, about 5.64% less. The giant cluster's single-compiler totals give 2.62 times faster and its largest peak falls to 25.9%, so
docs/development.md:326should not generalize the best individual result to the entire cluster. Also reconcile the 75 chunks in the method with the table's 15 + 37 + 24 = 76.There is runtime evidence, but not an uncapped game-speed measurement that establishes no cost. The two-function replays report ratios of 0.979 and 0.976; the four game runs stay at the 60 fps cap; the earlier sampling runs never sample the two changed giants. Each crossing adds a noinline call and return plus dispatch, and can force register reloads or prevent optimization across the cut. The short-loop rule minimizes crossings; it does not guarantee none. Eight KB of source does not mean eight KB of work executes on a branch-heavy path. With a hot list, the extracted hot files stay unsplit, but a function outside that list may be hot in another scene. Before recommending "no runtime cost found", measure a workload that actually spends time in the split functions, with the same hot-list/profile settings, named builds, two interleaved runs each, frame-time medians and spreads. Keep the reported sampler failure visible too.
Related work and limits
L_ff_<pc>label inside one outer block._local_gotosrecognizes its private jump and keeps the block whole. No float-region local or MXCSR window crosses a cut. That source-level compatibility does not settle the separate arithmetic and integration findings already posted on that PR.This head merges cleanly with main and with each of those two PRs separately. No full suite, Windows execution, Linux game build, actual combined build with either PR, replay of the recorded states, or runtime speed measurement was made here. The current source-check jobs are blocked, not passed: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/4496.
perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS)to WIP: perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS)Thanks, mooki. Back to work in progress while I address it:
emit_function(non-first entry, setjmp,A64_LOCAL_REGS/A64_LOCAL_FLAGSrefused, a float region), and a check throughcompile_onewith the split flag;The measured commit 012c615b is now on the fork as
perf/split-giants-measured(https://git.magicbean.dev/madebyjordan/Longshot/src/branch/perf/split-giants-measured), so the carry to the current head can be checked: the head adds only the rebase onto0caf0618and the default.Thanks for the careful read, mooki. Pushed
a4e125fdand9f91cf6c; the description is updated to match.Must change: the fixture's compiler.
tests/test_split_giants.pynow requires Clang, asbuild.pycompiles lifted code with clang on every OS.RUN_ALL_ARGS = ["--cc", "clang"]: run_all.py skips it without clang and fails it with--strict.--versionisn't Clang, fails the test, so it can no longer pass with every comparison skipped.Coverage, all compiled whole and split and run from the same 300 states with Clang 23: 0 differences in all 2,400 states.
Lifter.emit_function, withlift_fixesandpollinstalled, over a made-up image:host/hos_setjmp.c's assembly setjmp/longjmp: 233, 361 and 351 of the three programs' states resumed that way;A64_LOCAL_REGS/A64_LOCAL_FLAGSis left whole.build.py'scompile_onewith the split flag: the object holds the pieces, the copy is removed, and it runs the same as the whole function.Description:
Memory is now scoped:
docs/development.md's row says the same.75 vs 76: "the other 24" was 23 lifted chunks plus
code_signs.c, a runtime source whose name starts withcode_(0.1 s; no total changes).Runtime: "no runtime cost found" is gone. The description now gives what was measured: the replay ratios, best and mean, and the game runs at the 60 fps cap with no speed claim. It also says an uncapped scene where the split giants are hot wasn't found or measured.
The 1,634 s run is kept visible, with what supports leaving it out:
No per-process CPU record exists, so the cause isn't named.
#31: they compose at the source level, but the gains aren't necessarily additive.
The measured commit 012c615b is on the fork as
perf/split-giants-measured.Suite:
run_all.py --jobs 4, main @0caf0618485 / 6 / 70, this PR @9f91cf6c486 / 6 / 70. The same six Windows-only failures; the extra pass istest_split_giants.WIP: perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS)to perf(build): compile giant lifted functions in pieces cut at their labels, on by default (LONGSHOT_SPLIT_GIANTS)Second look at head
9f91cf6c. The must-change point of the first review is settled, so the change request no longer applies. Nothing new was found in the two follow-up commits. What stays open is evidence, not a defect: the real float-region emitter of pull request 38 combined with this splitter has not been executed, and there is no uncapped game-speed measurement with the pieces.Read head
9f91cf6c, both follow-up commits, the updated description and both replies, and compared the now-published measured commit012c615b; ran the changed check with Clang, GCC and a missing compiler, plus the same nine existing checks. Clang reports 0 differences across all 2,400 comparisons; GCC and the missing compiler are rejected as intended; the nine existing checks pass. No game, full build or timing run was made here.What's good
nx2/build.pynornx2/split_giants.pychanged since the first review. The follow-up changes tests and the documented claims, not the runtime transformation.Must change
The previous must-change point is settled.
tests/test_split_giants.py:45declaresRUN_ALL_ARGS = ["--cc", "clang"]; lines 420-437 accept only a compiler whose version identifies Clang; lines 528-534 fail instead of skipping when none is available.--cc gccnow exits 1 withno Clang to compile with (gcc), before compiling any comparison. A nonexistent compiler also exits 1. Clang passes the full check. The missing-Clang skip versus strict-failure routing is explicit intests/run_all.py:238-242,416-419; that runner policy was traced, not exercised with Clang removed.No new functional defect found in these two commits. The previous change request can be cleared.
Coverage requests
tests/test_split_giants.py:214-237usesLifter.emit_functionwith the real entry fix and polls. Lines 241-248 check the initial entry jump and splitting; lines 488-500 compile and compare three emitted functions. Instruction bodies are deliberately authored, as in the existing entry test, rather than translated from a dump.a64_icallremains a recording stand-in at line 340, so this establishes the split caller's wrapper behavior, not an end-to-end test of the game's exception routing.compile_onewith the split flagThe larger fixture is useful evidence, not an exhaustive proof for all giants or guest inputs. The remaining float-combination request was a coverage improvement, not the reason for the change request.
Measurements and smaller points
Memory wording: settled.
docs/development.md:326now distinguishes the cluster's 2.6-times single-compiler speed-up and largest peak of 2,263 to 586 MB, an individual giant's roughly one-fifth peak, and the whole build's roughly 6% summed-memory reduction. The description gives the corresponding medians, 5,129.5 to 4,840 MB. These match the arithmetic checked in the first review. The chunk count is also reconciled in the description as 15 + 37 + 23 lifted chunks; the extra name was a runtime source. The underlying per-file logs were not supplied for independent verification.Measured source availability and carry: settled. The published branch resolves to
012c615b557a0b680902e019517941c9fdbedc5e: https://git.magicbean.dev/madebyjordan/Longshot/src/branch/perf/split-giants-measured. Itsnx2/split_giants.pyis identical to this head. Innx2/build.py, the only difference is the default and its comments at lines 346-357: an unset switch now follows the same 8 KB path the measured build explicitly selected. This supports carrying the compile result for the same retained lift; it is not a newly measured complete build on the newer base.Excluded baseline: partly settled. Measurements now retains the 1,634 s run and supplies per-file slowdowns, variation across the build, free-memory readings and the absence of another lock-owning heavy job. That makes the exclusion more transparent. It still cannot identify the cause without the missing process/load record, and the text's “what says it was the PC” is stronger than that evidence warrants. Keep the exclusion labelled as an anomalous run rather than a proven diagnosis.
Runtime wording: settled; game-speed measurement: still open. The unsupported “no runtime cost found” recommendation is gone. Testing now describes the two-function replay timing method and explicitly says the capped route did not establish crossing cost in a workload where these functions are hot. There is no new uncapped game-speed measurement.
nx2/split_giants.py:194-239still adds the piece call/return and dispatch boundaries reviewed before. The 0.979 and 0.976 replay ratios are narrow evidence, not a whole-game speed guarantee; a same-profile, named-build comparison with repeated uncapped runs remains the way to settle that question.Count resume events, not distinct states.
tests/test_split_giants.py:412-414sums every resume, including multiple resumes in one state. The description's “361 ... of their states” should therefore say “361 resume events”. There are only 300 initial states per program. This is a wording correction, not a failing check.The description also correctly says that #31 composes at the source level without promising additive gains. Issue #362 still covers separate experiments, not additional results for this patch.
Not checked
No Windows execution here, full suite, real lift, game build, replay of the recorded game states, combined build with PR 38, or new performance measurement. The native comparisons and nine existing checks pass on Linux; the author's Windows results remain separately attributed. This head merges cleanly with main
0caf06184. Source-check jobs are still blocked, not green: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/4536.Approving at
9f91cf6c. The point of the first review is fixed (the new test requires Clang and fails without it), the follow-up added tests through the real emitter, a real setjmp return across a cut and the real compile entry, and the second look found nothing new: #385 (comment)This replaces the earlier change request. Two things remain worth having, as evidence and not as conditions: a game-speed measurement with the pieces in a scene where the split functions are hot, and the float-region emitter of #38 run together with this splitter.