Repository navigation
perf: faster prop gathering in roomGetProps (quadratic to linear) - #83
Open
andrewcaluzzi wants to merge 1 commit into
Open
andrewcaluzzi wants to merge 1 commit into
andrewcaluzzi wants to merge 1 commit into
Conversation
The roomGetProps function originally scanned a continually growing output list before inserting each prop, making each call O(N^2). This change introduces a small, static lookup to replace the rescan approach, so each call is O(N). A bounds check is also performed to avoid writing past the list, along with a range check for the propnum.
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.
Goal
NB: PC builds only; N64 builds compile the original code unchanged.
I've run up against performance issues while testing scenarios (in my fork) that include 1000+ characters/weapons (alive and persisted corpses). The bottleneck I found was when the engine gathers props in a list of rooms (by calling
roomGetProps) and becomes increasingly slower as the number of props increases.The goal is to pre-emptively improve the performance of this function ahead of modding/scripting support. The base game running on modern hardware doesn't encounter any performance issues because of the low prop count (dozens at most) and disappearing character corpses, but becomes the dominant cost once a room holds a large number of props.
I didn't open an issue first (as this can't be reproduced with the base game/engine) but I'm happy to do so instead.
Description
The
roomGetPropsfunction originally scanned a continually growing output list before inserting each prop, making each call O(N^2). Aperfprofile (in my fork test scenario) put about 98% of game-code time under the chr move test, and most of that wasroomGetProps's duplicate scan, called twice per chr per frame. Runtime spawning by mods and scripts, and more chrs in co-op/netplay, will run into the same thing.This change introduces a small, static lookup to replace the rescan approach, so each call is O(N). A bounds check is also performed to avoid writing past the list, along with a range check for the propnum.
One intended behaviour difference: past 511 distinct props in one call, the original wrote past the end of the list. Now the extra props are omitted from that query instead. "This isn't expected to happen on stock stages.
As disclaimed below, I've used Claude Opus 5.5 to diagnose/fix the issue, benchmark thoroughly, and interpret/provide the results.
Type of Change
How Has This Been Tested?
main(9062f9f).GE_DEMO=dam1andGE_DEMO=facility1withGE_DEMODIAG=1 GE_TICKHZ=60 GE_FRAMES=2400 GE_NOINISAVE=1, keeping the^ge: demo ticklines.mainproduce identical output: 453 lines for dam1 and 419 for facility1, withcmpsilent.cmpthe outputs.gcc -E -DTARGET_N64of chrprop.c with the Makefile's include paths and defines is token-identical before and after, for US, EU and JP.-Wall -Wextra, give the same counts before and after.Ways I tried to break it:
Benchmarks
All on an i7-8700K @ 3.7 GHz (Docker/WSL2, 32-bit gcc, the project's
-O2flags).1. Function micro-benchmark. The original and new function bodies run on identical room states built through the real register logic. Each figure is the median of 9 runs, and the output lists were checked identical. The original does about N²/2 comparisons per call; the new one is linear, at roughly 2 ns per prop.
It is faster at every size and never slower, including the few-dozen-prop rooms of the stock stages.
2. In game, at scale. This uses my fork's arena: one room holding about 1,300 props (crowd, kept corpses and live enemies). I compared the same fork build with and without only this change. The figure is game-code time per frame (the longest sim stretch).
The cost comes from moving chrs: each move test gathers every prop in the room, twice per chr per frame. With the original scan, 18 enemies in a 1,300-prop room took the frame from about 4 ms to about 57 ms. The fork also has fork-only changes that stop idle and dead chrs doing that work; those are in both builds above, so the difference is this change alone.
3. Stock stage. The dam1 demo plays the same with and without the change; no difference is visible. On stock stages the function costs microseconds per frame, so this is a correctness and scaling fix that costs nothing, not a stock-game speed-up.
The micro-benchmark source and the reproduction commands are available on request.
Checklist