Repository navigation
perf(shader): keep a specialized module's instructions in one arena - #7538
Conversation
Specialization parsed every instruction of the prepared module into its own std::vector, so a module of n instructions made n allocations when it was read and n frees when the specialization ended. Parsed instructions now take their words from a WordArena the Specialization owns, through an ArenaAllocator whose first chunk is the size of the module's words, so parsing allocates about once and the arena frees its chunks when the specialization ends. An allocator without an arena (Make's results and other temporaries) uses std::allocator as before, and assigning one to an instruction copies it into the arena. The specialized module is unchanged. This is a plain allocator rather than std::pmr because Apple's libc++ marks std::pmr unavailable before macOS 14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
|
The title capture the AI-assisted: yes (Claude Code) |
|
Checked the arena lifetime (declared before |
What
SpecializeSpirvparses the prepared module intoSpecialization::instructions, onestd::vector<std::uint32_t>per instruction. A module of n instructions therefore made n allocations as it was read and n frees when the specialization ended. In a profile of a Release replay of 355 MXGP 26 (PPSA30839) shader requests,SpecializeSpirvwas 7.9% of all samples onmain(inclusive), and about a third of that (211 of 607 samples) wasmallocandfree.Parsed instructions now take their words from a
WordArenathat theSpecializationowns, through anArenaAllocator(Instructionisstd::vector<std::uint32_t, ArenaAllocator<std::uint32_t>>). The arena's first chunk is the size of the module's words, so parsing allocates about once, and the arena frees its chunks when the specialization ends. The arena is declared beforeinstructionsandconstants, so it outlives them, and no instruction leaves theSpecialization:Runcopies the words into a plainstd::vector. An allocator without an arena usesstd::allocatoras before. That coversMake's results and other temporaries, and assigning one to an instruction copies it into the arena. Nothing else changes, and the specialized module is the same.I first wrote this with
std::pmr::monotonic_buffer_resource. That was about as fast (21% instead of 24%). But Apple's libc++ marksstd::pmrunavailable before macOS 14, and #7332's app bundle declares macOS 11 as its minimum, so this uses a plain allocator instead.Tested
Ubuntu 24.04 (Linux 6.8), GCC 13.3, NVIDIA RTX 4070 Ti, driver 595.99.02.
ctest -R "^agc_|^shader_ir"on 6efb726: 710/710 pass (3 skipped: host import sparse, sparse dma-buf import, stencil ref export).agc_shader_replay --spv(Release,ANYPS5_ENABLE_SPIRV_TOOLSoff,ANYPS5_NO_SHADER_CACHE=1) on 466 shader requests I had dumped withAPS5_DUMP_SHADERS: 355 from PPSA30839, 107 from PPSA31162 and 4 from another app. The replay writes the specialized module. All 462.spvfiles (a few names repeat across the dumps) have the same SHA-256 onmain0d078f7 and with this PR, and the replay output is identical. This PR is on 6efb726; the 9 commits between the two don't touchcore/shader/recompiler.Specialization time, from a temporary timer around
SpecializeSpirvthat is not part of this PR. It is summed over the 319 calls of one replay of the 355 PPSA30839 requests, 3 runs per build, interleaved:main0d078f7That is about 24% of the specialization time, or about 0.5 s of the set's roughly 25 s replay. Whole-replay times during these runs swung with another workload on the host (27.4 to 28.6 s for
main), so I'm only claiming the specialization time.Title level: no speed claim. Specialization runs when a draw needs a module for new pipeline state, so this shortens that wait, but I didn't measure it in a title.
tools/check_conventions.py --base upstream/mainpasses.tools/pr_overlap.pylists only #6176, which conflicts withmainso it could not compare it. #6176 adds tables to the constructor one line above the parse loop this PR changes. #5315 changesremoveUnusedConstants. Neither touches these lines.Title capture
As the
perflabel asks, these are captures of one title and scene on one machine. MXGP26 (PPSA30839) ran from boot withAPS5_TRACE_SECONDS=0:60,ANYPS5_NO_SHADER_CACHE=1and a fresh run directory (cold AnyPS5 and NVIDIA caches), so the shaders compile inside the window. NVIDIA RTX 4070 Ti (595.99.02), Ubuntu 24.04, release libs, Xvfb display, no input.maindbac9fa was captured five times: before the PR captures, after them, and three more times in a row after that. Each PR capture and the first twomaincaptures followed a rebuild of the release libraries; the last threemaincaptures did not, and they came out lower. The change ismainwith this PR merged on top as a local merge commit (e449aef241c0). The time is the wall time of theShader.PrepareArtifactslices on theshader preparethreads in each trace.main1main2main3main4main5The five
maincaptures span 17.7% in total prepare time, and the 13 prepare threads run in parallel with the title, so a title capture can't resolve a recompiler change of a few percent. The replay measurement above is this change's measurement. This PR's capture is above all fivemaincaptures in total prepare time. With one capture of the change I can't tell that apart from noise, and the replay measurement above points the other way. I can repeat the capture if that would help.main1 trace, digest,main2 trace, digest,main3 trace, digest,main4 trace, digest,main5 trace, digest, this PR trace, digest{"commit":"dbac9fa15b61","dirty":false,"env":{"APS5_TRACE_SECONDS":"0:60"},"gpu":"NVIDIA GeForce RTX 4070 Ti","driver":"NVIDIA 595.99.02","os":"Linux 6.8.0-147-generic","resolution":"3840x2160","window":{"spec":"seconds 0:60","first_present":0,"ms":120148.301},"presents":194,"frame_ms":{"p50":16.686,"p90":32.765,"p99":2921.590,"max":112847.624},"gpu_busy_ms_per_present":6.483,"worker_busy_pct":{"queue worker 0x0":88.481,"queue worker 0x20":73.127},"top_phases_ms_per_frame":{"Driver.Packet":1000.862,"Driver.Packet.dispatch":503.561,"Driver.Dispatch":503.451,"Shader.PrepareArtifact":450.125,"Driver.Packet.memory_wait":424.003,"Vulkan.GraphicsPipeline":132.519,"Vulkan.GraphicsPipelineLibrary":126.497,"Vulkan.GraphicsPipelinePrebuild":77.312},"trace_dropped":false,"trace":"aps5-trace-dbac9fa15b61-3.json.gz"} {"commit":"e449aef241c0","dirty":false,"env":{"APS5_TRACE_SECONDS":"0:60"},"gpu":"NVIDIA GeForce RTX 4070 Ti","driver":"NVIDIA 595.99.02","os":"Linux 6.8.0-147-generic","resolution":"3840x2160","window":{"spec":"seconds 0:60","first_present":0,"ms":118089.051},"presents":188,"frame_ms":{"p50":16.688,"p90":29.330,"p99":2332.068,"max":111625.858},"gpu_busy_ms_per_present":5.809,"worker_busy_pct":{"queue worker 0x0":88.437,"queue worker 0x20":71.533},"top_phases_ms_per_frame":{"Driver.Packet":1004.814,"Shader.PrepareArtifact":518.836,"Driver.Packet.dispatch":513.181,"Driver.Dispatch":513.126,"Driver.Packet.memory_wait":418.222,"Vulkan.GraphicsPipeline":135.265,"Vulkan.GraphicsPipelineLibrary":129.624,"Vulkan.GraphicsPipelinePrebuild":80.289},"trace_dropped":false,"trace":"aps5-trace-e449aef241c0-3.json.gz"}Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code