Repository navigation
perf(shader): intern SPIR-V type and constant ids without a per-call allocation - #327
Conversation
|
Nice speedup, and I checked that the cache can only ever answer with an id the |
…allocation Every SpirvModule::Type and Constant call built a heap std::vector key and looked it up in a std::map keyed on std::vector, so the comparison walked the key bytes. At the one to three such calls per emitted instruction a shader issues thousands of them per compile. Add a fixed-size, linearly probed declaration cache that holds the key inline and is consulted before the key vector is built, making a repeated declaration a hash and a compare. The cacheable shapes are those whose operands are a word: OpTypeVoid, OpTypeBool, OpTypeInt, OpTypeFloat, OpTypeVector, OpTypePointer, OpTypeStruct, OpTypeArray, OpTypeFunction and the scalar OpConstant family. The overloads are constrained on word-sized operands so that enum operands such as spv::StorageClassFunction reach the cache instead of binding the variadic template by reference. Measured with a harness driving the module through the emitters' call shapes: 80000 hot type and constant calls drop from 80074 heap allocations to 140. The assigned ids and the emitted SPIR-V word stream are byte-identical. Co-Authored-By: Claude <noreply@anthropic.com>
…test OpConstantTrue and OpConstantFalse carry no operand word, so they reached the declaration cache through the variadic Constant template, which builds the key vector, and paid an allocation on every call. ConstantBool emits both, so a shader paid it twice per boolean constant. Add the two-word overload; the compile-shaped benchmark drops from 10413 allocations to 415. Add agc_spirv_module_tests covering the interning contract: a repeated declaration returns one id and is emitted once, operand shapes that share a leading key word stay distinct, integral and enum operands agree, decorated types stay separate from plain ones, memoized ids survive AllocateId, modules do not share ids, and ids stay unique and contiguous past the cache capacity. The test passes against both the previous header and this one, so it pins the contract rather than the implementation. Benchmark, pre-optimization header against this one, best of five: hot loop x 2M 613.6 ms -> 46.3 ms 13.2x compile-shaped 5000 instr 4.2 ms -> 0.4 ms 10.8x 64 compile-shaped modules 286.0 ms -> 25.4 ms 11.3x Allocations for the compile-shaped case fall from 55217 to 415. Both builds compute the same value over the same call sequence. Co-Authored-By: Claude <noreply@anthropic.com>
|
Fixed, and the branch is now rebased onto current
The test passes on both compilers. No games tested, and the full CMake build was not run locally. Two things worth knowing. The Build run for this branch is at Unrelated to this PR, and not touched: |
|
Thanks, all three are done, and the first two commits came through the rebase unchanged. Ready to merge from my side once CI has run on this head. |
|
Gated on PPSA21564 three times after the shader caches refilled: no visual or FPS change. One thing since: #611 (mine) added its test next to yours in |
# Conflicts: # core/libs/prx/libSceAgcDriver/CMakeLists.txt
|
All done and ctest is 289/289. |
|
Thanks. Your four commits are unchanged from One small nit, not blocking: the resolved |
|
Done. |
|
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.
|
|
Checked that the cache only returns ids |
What
OpConstantTrue and OpConstantFalse carry no operand word, so they reached the
declaration cache through the variadic Constant template, which builds the key
vector, and paid an allocation on every call. ConstantBool emits both, so a
shader paid it twice per boolean constant. Add the two-word overload; the
compile-shaped benchmark drops from 10413 allocations to 415.
Add agc_spirv_module_tests covering the interning contract: a repeated
declaration returns one id and is emitted once, operand shapes that share a
leading key word stay distinct, integral and enum operands agree, decorated
types stay separate from plain ones, memoized ids survive AllocateId, modules
do not share ids, and ids stay unique and contiguous past the cache capacity.
The test passes against both the previous header and this one, so it pins the
contract rather than the implementation.
Benchmark, pre-optimization header against this one, best of five:
hot loop x 2M 613.6 ms -> 46.3 ms 13.2x
compile-shaped 5000 instr 4.2 ms -> 0.4 ms 10.8x
64 compile-shaped modules 286.0 ms -> 25.4 ms 11.3x
Allocations for the compile-shaped case fall from 55217 to 415. Both builds
compute the same value over the same call sequence. Measured before this branch
was rebased onto current
main; the surrounding call sites have changed since,so read the ratios as indicative.
Tested
Windows 11.
agc_spirv_module_testsbuilt and run standalone (SpirvModule.cppplus
tests/SpirvModule.cpp, no other dependencies) on MSVC 19.44/W4andMinGW GCC 15.2 with
-Wall -Wextra; all ten cases pass on both. The GCC buildreproduces the lookup-order error the previous revision had at the three
requiresclauses and compiles clean once the helpers move above them. FullCMake build and ctest not run locally. No games tested.
Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)