Repository navigation
perf(host): three per-call costs on hot paths removed (own-thread lookups, a Signal with no waiter, switches read once) - #2708
klimovich008 wants to merge 6 commits into
Conversation
…skips the scan of every thread thread_arg (GetThreadPriority, GetThreadId, ChangeThreadPriority...) scanned every thread under threads_lock; the game asks about its own thread most of the time (GetThreadPriority(GetCurrentThread()), ~2,400 a frame in Hyrule). The calling thread's record answers that, compared with its current ThreadType. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The waiter count is read atomically before the lock pair and wake call; waiters count themselves in under the condition variable's lock before they give the guest mutex back, so a signaller holding the mutex sees them. HOS_CV_SIGNAL_ALWAYS=1: as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
menu_vsync (two getenv a present), HOS_FS_OVERLAY (every OpenFile) and RX_VK_DUMP_STAGES (every code read) asked the environment each time. They are set for a whole run; the V-Sync fixture now runs a process per switch setting. (RX_VK_TAAU_KEYS stays per dispatch: tests/test_taau_env_lookup.py pins that read, about once a frame.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(sol review: two first OpenFile calls could see the flag set before the pointer.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(sol review round 2: the first signals of several threads read and wrote it at once.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_shader.cpp is hashed into the shader-cache stamp (nx2/build.py), so any change there makes every player's translated programs and prepared pipelines stale: a full re-prepare for a debug switch on the translation path, which does not run in play once programs are prepared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Joey-Magnus
left a comment
There was a problem hiding this comment.
I merged this into current main (51da046) and ran tests/run_all.py: everything passes except test_lift_cache.py, which fails on main here too. The new test_thread_arg_self.py and test_cv_signal_early.py pass, and so does the reworked test_vsync_row.py.
I checked the Signal early return closely, since a missed wake would hang a thread. cv_enter counts the waiter in under cv->lk before mutex_release_all, and the gen snapshot is taken there too. So a signaller that skips at host/nn_os_sync.c:605 is ordered before the waiter's entry, and the old code would have missed that waiter in the same way. thread_arg's shortcut returns the same record hos_thread_of would. The once-read switches are run-wide.
Two notes. The Measured section still says provisional, on 57ed3aa and only together with #2709; please put in the re-measure on current main when you have it. #2765 adds BroadcastConditionVariable right below Signal. It merges cleanly with this, but it could take the same no-waiter return later.
The game calls a few nn::os and host paths thousands of times a frame, and each call does work whose answer is already known. Each change below removes that work and keeps its result. They are small, and together they are hard to measure apart (see Measured).
The change
host/hos_thread.cthread_arg). GetThreadPriority, GetThreadId, ChangeThreadPriority and the others scanned every thread underthreads_lock. The game asks about its own thread most of the time (GetThreadPriority(GetCurrentThread()), about 2,400 a frame in Hyrule Field).host/nn_os_sync.c). It returns at once when the condition variable's waiter count is 0, without the lock pair and wake call.HOS_CV_SIGNAL_ALWAYS=1: as before.cv_enter, a seq-cst add under the condition variable's lock) before it gives the guest mutex back (mutex_release_all). A signaller that holds the mutex therefore sees the waiter, and the waiter cannot be skipped.menu_vsync(RX_VK_FIFO/RX_VK_IMMEDIATE, twice a present) andHOS_FS_OVERLAY(every OpenFile). They hold for a whole run. Each is now read once and published atomically (HOS_FS_OVERLAYas one atomic pointer).RX_VK_DUMP_STAGESstays a per-readgetenv:vk_shader.cppis part of the shader-cache stamp, so changing it would make every player's prepared shaders stale.RX_VK_TAAU_KEYSstays a per-dispatch read:tests/test_taau_env_lookup.pypins that.Measured
Provisional: measured on 57ed3aa, which predates #2602's fix of a main regression (41 → 53 fps on this test); to be re-measured on 4678050.
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, 20 s window, warm caches, 6 alternating pairs, base main 57ed3aa.These changes were measured only together with #2709: CPU a frame 128.4 → 122.8 ms (−4.4 %), fps 39.6 → 40.1, no frames over 100 ms, no vcore escapes, game clock 1.000. The arms with only one of #2709's two changes and
HOS_CV_SIGNAL_ALWAYS=1read 124.5–124.9 ms. That does not separate the Signal change from the other two, and the own-thread lookup and the switch reads have no switch to compare. This PR's own share is not measured.Tested
tests/test_thread_arg_self.py(new):thread_argfrom the source, over own and other ThreadTypes. Broken variants must fail: every call scanning, the caller's record returned for any ThreadType.tests/test_cv_signal_early.py(new): Signal over waiter counts, with and withoutHOS_CV_SIGNAL_ALWAYS. Broken variants must fail: the early return taken even withHOS_CV_SIGNAL_ALWAYS=1, the early return reading the wrong count, a waiter that does not count itself in. A structural check:cv_entercomes beforemutex_release_allin both waits.tests/test_vsync_row.py: one process per switch setting now (the value is read once), with a negative control.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