From 92fb08719e706b26caeea53f6568c9683cb1b02e Mon Sep 17 00:00:00 2001 From: Jessica_Natalia Date: Sun, 16 Aug 2026 01:28:40 -0300 Subject: [PATCH] =?UTF-8?q?OTIMIZA=C3=87OES=20ROUND=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OTIMIZAƇOES ROUND 2 --- include/psprecomp/runtime.hpp | 13 ++++++ profiles/vcs/CMakeLists.txt | 48 ++++++++++++++------ profiles/vcs/host/main.cpp | 1 + profiles/vcs/scripts/build_release_ninja.bat | 2 +- src/runtime.cpp | 30 ++++++++++++ 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/include/psprecomp/runtime.hpp b/include/psprecomp/runtime.hpp index 6c426e0..ca673d9 100644 --- a/include/psprecomp/runtime.hpp +++ b/include/psprecomp/runtime.hpp @@ -65,6 +65,18 @@ void set_runtime_thread_identity(std::int32_t uid, const std::string &name) noex [[nodiscard]] std::uint64_t runtime_thread_switch_generation() noexcept; [[nodiscard]] bool runtime_thread_switch_generation_matches(std::uint64_t generation) noexcept; +// Per-generated-unit call census, armed with PSPRECOMP_UNIT_PROFILE=1. +// +// Exists to answer one question the build cannot answer on its own: which of the +// 234 generated units are actually hot, so VCS_HOT_UNIT_IDS can name them +// instead of carrying a single hand-picked entry. Counting is a load of one +// global bool and a predictable branch on the chained-call path, so leaving it +// compiled in costs nothing measurable when it is off. +inline constexpr std::size_t kUnitProfileCapacity = 512u; +extern bool g_unit_profile_enabled; +extern std::uint64_t g_unit_profile_counts[kUnitProfileCapacity]; +void report_unit_profile(std::size_t limit = 40u); + class Runtime { public: using RecompiledFunction = void (*)(Runtime &, AllegrexContext &); @@ -216,6 +228,7 @@ public: } else { Function(*this, ctx); } + if (g_unit_profile_enabled) ++g_unit_profile_counts[UnitIndex]; #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (track_dispatch_counters_) { ++chained_dispatches_; diff --git a/profiles/vcs/CMakeLists.txt b/profiles/vcs/CMakeLists.txt index 75f2225..fd98888 100644 --- a/profiles/vcs/CMakeLists.txt +++ b/profiles/vcs/CMakeLists.txt @@ -20,15 +20,23 @@ endif() # Full /Ob3 is valuable at runtime but several large, cold units still make # MSVC spend minutes and close to a gigabyte per compiler process. Keep those # units buildable, then override this level only for the measured hot corpus. -# /Ob0 does not merely decline to auto-inline: MSVC drops __forceinline with it -# too, verified by compiling a stand-in accessor at each level and reading the -# disassembly -- /Ob0 emits a call to the accessor itself, /Ob1 and above emit a -# call only to its cold fallback. Every guest load and store in the recompiled -# corpus goes through a __forceinline fast path, so /Ob0 turned the single most -# frequent operation in the program into an out-of-line call. /Ob1 restores it -# while still refusing the aggressive auto-inlining that made /Ob3 blow up the -# optimizer on the larger units. -set(PSPRECOMP_GENERATED_INLINE_LEVEL "1" CACHE STRING +# Keep this at 0, and do not "fix" it without measuring. +# +# /Ob0 does drop __forceinline, not merely auto-inlining: compiling a stand-in +# accessor at each level and reading the disassembly shows /Ob0 emitting a call +# to the accessor itself while /Ob1+ emit a call only to its cold fallback. Since +# every guest load and store goes through a __forceinline fast path, /Ob0 looks +# like it must be turning the most frequent operation in the program into an +# out-of-line call, and /Ob1 looks like a free win. +# +# It measures 13.6% slower. On a 3800-frame route, median frame 11.84 ms at /Ob0 +# against 13.69 ms at /Ob1 (median FPS 84.5 against 73.0), guest cpu_us 9.66 ms +# against 11.54 ms. Inlining the fast path at every call site grows the binary +# from 103 MB to 232 MB, and this corpus is already hostile to the instruction +# cache: 234 units, ~50k cross-unit chains, over a million architectural register +# references. The call overhead removed is smaller than the I-cache and BTB +# pressure added. +set(PSPRECOMP_GENERATED_INLINE_LEVEL "0" CACHE STRING "MSVC /Ob level for generated VCS AOT units (0, 1, 2 or 3)") set_property(CACHE PSPRECOMP_GENERATED_INLINE_LEVEL PROPERTY STRINGS 0 1 2 3) if(NOT PSPRECOMP_GENERATED_INLINE_LEVEL MATCHES "^[0-3]$") @@ -66,12 +74,24 @@ else() endif() if(PSPRECOMP_PROFILE_GUIDED_AOT) - # This unit is hot in the current capture and finishes /Ob3 without - # triggering MSVC's pathological optimizer growth. It also contains the - # validated native collision leaf at guest PC 0x088B1554. Other measured - # units remain /Ob0 until their oversized helpers are split/noinline. + # The units that carry 80% of the guest's cross-unit calls, measured with + # PSPRECOMP_UNIT_PROFILE=1 over a 7900-frame route: 141 million calls across + # 201 live units, roughly 17,900 unit transitions per frame. + # + # Only 0043 used to be here, and the census puts it eighth at 3.3% -- the + # aggressive settings were reaching 3% of the work. 0023 and 0024 alone are + # 16.6%. + # + # Why a list and not the whole corpus: /Ob1 applied to all 234 units measured + # 13.6% *slower* (median frame 13.69 ms against 11.84 ms) because the binary + # grew from 103 MB to 232 MB and this corpus is already hostile to the + # instruction cache. Confining the aggressive settings to the hot 16% keeps + # the code growth bounded where it cannot pay for itself. set(VCS_HOT_UNIT_IDS - 0043) + 0023 0024 0097 0030 0011 0091 0084 0043 0067 0163 + 0087 0142 0066 0152 0109 0179 0085 0215 0064 0149 + 0095 0199 0092 0206 0022 0190 0200 0080 0164 0198 + 0088 0155 0213 0154 0169 0035 0089) set(VCS_HOT_SOURCES) foreach(VCS_UNIT_ID IN LISTS VCS_HOT_UNIT_IDS) set(VCS_UNIT_SOURCE "${VCS_PROFILE_DIR}/generated/generated_unit_${VCS_UNIT_ID}.cpp") diff --git a/profiles/vcs/host/main.cpp b/profiles/vcs/host/main.cpp index 58d93dd..fd78937 100644 --- a/profiles/vcs/host/main.cpp +++ b/profiles/vcs/host/main.cpp @@ -307,6 +307,7 @@ int main(int argc, char **argv) { psprecomp::report_counted_pcs(); if (shutdown_diag) std::cerr << "[shutdown] counted-pcs-reported\n"; runtime.report_hle_histogram(); + psprecomp::report_unit_profile(); if (shutdown_diag) std::cerr << "[shutdown] hle-histogram-reported\n"; vcs::report_disc_read_stats(); vcs::report_present_stats(); diff --git a/profiles/vcs/scripts/build_release_ninja.bat b/profiles/vcs/scripts/build_release_ninja.bat index 3d850f8..aa1f120 100644 --- a/profiles/vcs/scripts/build_release_ninja.bat +++ b/profiles/vcs/scripts/build_release_ninja.bat @@ -122,7 +122,7 @@ echo [1/7] Configuring persistent Ninja Release tree... -DPSPRECOMP_MSVC_MP_JOBS=1 ^ -DPSPRECOMP_PROFILE_GUIDED_AOT=ON ^ -DPSPRECOMP_HOT_GENERATED_OPT_LEVEL=3 ^ - -DPSPRECOMP_GENERATED_INLINE_LEVEL=1 ^ + -DPSPRECOMP_GENERATED_INLINE_LEVEL=0 ^ -DPSPRECOMP_HOT_GENERATED_INLINE_LEVEL=3 ^ -DPSPRECOMP_VCS_AOT_LTO=OFF ^ -DPSPRECOMP_BUILD_TESTS=ON ^ diff --git a/src/runtime.cpp b/src/runtime.cpp index 693fedb..c2d5f63 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +141,7 @@ Runtime::Runtime(std::uint32_t ram_size) : memory_(ram_size) { // in the middle of guest execution; larger profiles can still grow it. import_bindings_.resize(256u, nullptr); hle_histogram_enabled_ = std::getenv("PSPRECOMP_HLE_HISTOGRAM") != nullptr; + g_unit_profile_enabled = std::getenv("PSPRECOMP_UNIT_PROFILE") != nullptr; #if defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) track_dispatch_counters_ = false; #else @@ -304,6 +306,7 @@ bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index if (unit_index >= kGeneratedUnitFastCapacity || !generated_unit_layout_valid_) return false; RecompiledFunction function = generated_units_[unit_index]; if (function == nullptr) return false; + if (g_unit_profile_enabled) ++g_unit_profile_counts[unit_index]; const std::uint32_t target_pc = ctx.pc; const std::uint32_t native_depth = chain_depth_; @@ -384,6 +387,33 @@ std::vector> Runtime::hle_histogram() cons return entries; } +bool g_unit_profile_enabled = false; +std::uint64_t g_unit_profile_counts[kUnitProfileCapacity]{}; + +void report_unit_profile(std::size_t limit) { + if (!g_unit_profile_enabled) return; + std::vector> entries; + std::uint64_t total = 0u; + for (std::size_t index = 0; index < kUnitProfileCapacity; ++index) { + if (g_unit_profile_counts[index] == 0u) continue; + entries.emplace_back(g_unit_profile_counts[index], index); + total += g_unit_profile_counts[index]; + } + std::sort(entries.begin(), entries.end(), std::greater<>()); + std::cerr << "[unit-profile] units=" << entries.size() << " calls=" << total << "\n"; + std::uint64_t running = 0u; + for (std::size_t i = 0; i < std::min(limit, entries.size()); ++i) { + running += entries[i].first; + std::cerr << "[unit-profile] unit=" << std::setw(4) << std::setfill('0') + << entries[i].second << std::setfill(' ') + << " calls=" << entries[i].first + << " share=" << (100.0 * static_cast(entries[i].first) / + static_cast(total ? total : 1u)) + << "% cumulative=" << (100.0 * static_cast(running) / + static_cast(total ? total : 1u)) << "%\n"; + } +} + void Runtime::report_hle_histogram(std::size_t limit) const { if (!hle_histogram_enabled_) return; const auto entries = hle_histogram();