perf(host): fewer idle game threads poll through short sleeps and spins on 7+ thread CPUs #2709
Closed
github-sync
wants to merge 6 commits from
github/pr/2709 into main
pull from: github/pr/2709
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2709
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2709"
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?
On CPUs with more than six logical processors, OoT's idle job workers each keep a processor busy. They poll in 20 µs steps through a yielding short sleep, and spin 20 µs before every event, queue or mutex wait sleeps. This PR limits how many of them poll at once, and shortens their spin, so the frame uses less CPU for the same picture.
The change
Both are default on, with a switch to restore the old behaviour. Windows only for the first; the second is on every platform.
host/hos_thread.csleep_ns). Where the game's sleeps under 1 ms yield (Windows, 7+ logical processors; ≤6 already use the timer since #2263), at mostHOS_SHORT_SLEEP_POLLERSgame threads at a time yield through one. The default is a quarter of the logical processors (4 on 16). The next one waits on the high-resolution timer until a slot is free.HOS_SHORT_SLEEP_POLLERS=0: all yield, as before.HOS_SHORT_SLEEP_TIMERset: no limit (it already decides for all).hos_spin_ns, used bynn_os_sync.candhos_svc.c). The serial threads keepHOS_SPIN_US(20 µs). The others spinHOS_SPIN_OTHER_US(5 µs, never more thanHOS_SPIN_US).HOS_SPIN_OTHER_US=20: one spin for all, as before.hos_thread_set_name); the flag is atomic. The choices are read once and published as one atomic word each.Measured
Provisional: measured on
57ed3aa8, which predates #2602's fix of a main regression (41 → 53 fps on this test); to be re-measured on46780503.Windows 11, Ryzen 7 5700X3D (16 logical processors), RX 9070 XT, AMD driver 26.10.2. Standard Hyrule Field test: right stick held right from the load,
HOS_FPS_MAX=240, 2560x720 (32:9), 20 s window, warm caches. Medians of 6 alternating pairs. Base is main at57ed3aa8; the candidate is the same tree with this PR and #2708 merged.8 and 6 threads
The same test, with the game held to 8 (
0xFF) or 6 (0x3F) logical processors from its start. Each cell has 6 alternating pairs. Base is main57ed3aa8; the candidate is this PR + #2708, built as packs over it.Tested
tests/test_short_sleep_timer.py(extended): the limit's choice overHOS_SHORT_SLEEP_POLLERSvalues and processor counts, run on the realsleep_nsblock. Broken variants (no limit, serial threads counted, a leaked count) must fail.tests/test_spin_by_role.py(new):hos_spin_nsfrom the source over the switch values, including negatives and huge values. Broken variants (no role, no clamp, no off, the Presentation Thread not serial) must fail. Wiring checks: every spin askshos_spin_ns.tests/run_all.py; syntax compile of every changedhost/*.c; a gpt-6.1-sol review (three rounds, last clean).Not tested
🤖 Generated with Claude Code
The 8-thread (limit 2) and 6-thread cells of the same Hyrule Field test are queued; I'll add them to the description with fps, CPU a frame and long-frame counts. If 8 threads shows more long frames, the default goes off below 13 threads.
Tested
75ce5c0con a 24-thread i7-13700K (8P+8E), RTX 5090 / NVIDIA 621.14, Windows 11, 96 GB RAM. This isolates #2709 on mainc544828f, without #2708, with the same renderer, image, warm caches and copied saves/options.Kokiri Forest autosave, stationary, 2048x1152, frame generation off,
HOS_FPS_MAX=240, windowed/muted/scripted input. Warm-up per arm, then three rounds in ABC/CBA/ABC order, followed by three more cap2/main pairs in BA/AB/BA order (six pairs total, balanced order). Frame statistics: 40-70 s after first present; process CPU: wall 45-75 s after launch.Defaults, three matched runs each:
Cap 2, six matched runs each:
HOS_SHORT_SLEEP_POLLERS=2Defaults saved only 0.09 cores here, within run-to-run noise; the five ModuleSystemWorker threads' CPU barely changed. With cap 2, the saving was 0.623 cores (10.4%), beyond the shared comparison's 2-SD noise gate, and each worker fell 10.9-12.1 percentage points of one core. The FPS and tail changes remain below that gate, but cap 2's mean FPS was 1.7% lower, with four of six paired deltas negative. This is not proof of no FPS regression, and I would not use this result to tighten the default. No errors, >50 ms frames, stalls, or concurrent-game/compiler interference in either PR batch. Linux gameplay/Steam Deck not tested here.
One Clang 23 fixture issue: the
no-clampmutation generatesother = other;, which fails compilation under-Werror,-Wself-assigninstead of exercising the broken runtime case. Removing the whole clamp statement fixes that without suppressing a warning or weakening the check:With that fixture-only repair, the Windows full suite gives 456 passed / 35 skipped / 1 unrelated baseline failure (
test_input_background.py, Clang 23 unusedoc_consumed_pads). Both of this PR's fixtures pass.A 12-thread cell (limit 3), on NVIDIA: the same frame rate for about 9% less CPU.
Windows 11, Ryzen 5 5600X (6 cores, 12 logical processors), RTX 3060 Ti 8 GB (driver 32.0.16.2114), 2560x1440 DLSS.
mainba005ca4with #2711, #2731 and #2685 merged locally (both arms), plus this PR (without #2708). A/B withHOS_SHORT_SLEEP_POLLERS=0 HOS_SPIN_OTHER_US=20as the old behaviour. Hyrule Field child, 40 s of the camera turning after a settle,RX_VK_FRAMEGEN=0,HOS_FPS_MAX=240, warm caches, arms alternated. CPU is the game process's user + kernel time over the measured window (GetProcessTimes), divided by the frames shown.=0,=20)No more long frames with it than without on 12 threads. The third old run is the slowest of the six; without it the fps arms are level and CPU a frame is still ~6% lower.
I merged this with #2708 into current main and ran tests/run_all.py. Only test_lift_cache.py fails, and it fails on main here too. test_short_sleep_timer.py and test_spin_by_role.py pass with clang 21. The code reads correctly. The poller count is always decremented, the choice and the spin lengths are each published as one word, and Linux gets no cap because threads stays 0 there.
This turns on by default for every Windows player with more than 6 logical processors, so I think a maintainer should decide on the evidence:
Still open from Brett-D: the no-clamp mutation at tests/test_spin_by_role.py:130 produces
other = other;, which fails to compile under clang 23 with -Werror,-Wself-assign. His replacement fixes it. Also, in host/hos.h:113 the "one of the frame's serial threads" comment trails hos_spin_ns instead of hos_thread_serial.The miss counters that turn a spin off are per object (
o->spin_missesin nn_os_sync.c,misses[slot]in hos_svc.c), not per thread. Now a worker's 5 us spin that misses also counts toward the 4 misses that skip the spin 31 times in 32, and that includes the game thread's 20 us spin on the same mutex or event. Could that turn the serial threads' spin off on the job queues' shared objects more often than before? The fps numbers don't show it, but it's cheap to rule out. One option is to count a miss only when the spin was the fullHOS_SPIN_US.Nit: a thread that loses here still bumps the count for a moment, so two losers at once can push a third thread to the timer while a slot is actually free. It's harmless (it just means one poller fewer for a moment), so this is only a note, no change needed.
Thanks, this reads well. Nothing here trades a signal for a sleep, so there's no lost-wakeup risk: a capped poller still waits on the timer to the same deadline, and where the high-resolution timer can't be made it falls through to the old yield loop. The 5 us spin only shortens the spin in front of a wait that already has its wake path. The poller count is always given back, both choices are published as one word, and Linux and the Steam Deck are untouched:
process_threads()is 0 there, so no cap and one spin for all. Since7c42bc63that also holds at 6 or fewer threads on Windows, so those CPUs now behave exactly like main. That settles the unexplained 608 ms frame from the 6-thread cell that Joey-Magnus raised.Joey's two points (Brett-D's clang 23
-Wself-assignfix for the no-clamp mutation, and the hos.h comment) are fixed in2e6de23f.Before I approve:
57ed3aa8before #2602, and the main tables are this PR + #2708 together. Please run one 16-thread Hyrule cell with 6 alternating pairs on46780503(or later) with this PR only: fps, CPU ms a frame, p99, worst, long frames. Brett-D's 24-thread run (defaults within noise) and klikso's 12-thread run (~6-9 % less CPU at the same fps) are good support, but neither is on the current base.7c42bc63. It still says the 5 us spin is "on every platform", and the 1.6 % fps cost at 6 threads that the new code comment cites isn't in the description. Please add that second 6-thread cell, and the 8-thread cell if you re-run it.CI: the earlier red run on
2e6de23fwastest_release_scripts.py(the nightly two-runs race), which this PR doesn't touch, so not yours. The run on7c42bc63is still pending. It merges cleanly with main. The one change tohos_thread.csince your base (#2805) is in another part of the file.Overlap: #2708 only adds lines next to the end of
sleep_ns, and #2765 and #2858 change other parts ofnn_os_sync.c. None conflict textually. #2487 is POSIX only. I'd land #2708 and #2765 first, then this, so your measured base (which had #2708) matches what ships. #2858 is still a draft and can go after.