Skip to content

perf(libc): start the guest allocation overlap queries at the range that touches the address - #6321

Open
mariuszkreft wants to merge 2 commits into
boykopovar:mainfrom
mariuszkreft:wip/gta-v-4634-registry-scan
Open

mariuszkreft wants to merge 2 commits into
boykopovar:mainfrom
mariuszkreft:wip/gta-v-4634-registry-scan

Conversation

@mariuszkreft

@mariuszkreft mariuszkreft commented Oct 15, 2026 •

Copy link
Copy Markdown

What

GuestAllocationsOverlaps and GuestAllocationsRequireAvailable walked the whole guest
allocation range registry from begin(), so every mapping-sized query paid for every
registration below the queried address. #4634's samples put the sceKernelBatchMap2
threads' time in exactly these two calls, reached from DoMapDirect, with ~35,000 live
64 KiB direct mappings in the registry.

Both now start at firstRangeTouching(address): the first range at or after the address,
or the one before it when that one reaches into the address. The step back is the point — a
query landing inside a registered range is the overlap these functions exist to report, and
a scan that starts at lower_bound alone misses it. GuestAllocationsAcquire also reserves
the registry size before filling its vector.

  • Only the scan start changes. Same results, same refusals, same messages.
  • Acquire is unchanged in behaviour: the vector is still rebuilt when its weak_ptr cache
    was reset, which a mutation does. Making that cheaper needs the pin test to stop relying
    on use_count(), which is what perf(memory): restrict registry work to affected ranges #1324 is about.

Tested

Linux, Ubuntu 24.04, GCC 13.3, Release, full build (-DBUILD_TESTING=ON) on this branch
wip/gta-v-4634-registry-scan over origin/main:

  • ctest --test-dir build --output-on-failure -j 12 → 1233/1234 passed.
    The single failure, agc_pending_key_stores, reproduces on clean origin/main on this
    host: it needs host write protection, and the NVIDIA driver here reports
    host imports resolve write protection: no (0 of 4 scratch pages written).
  • guest_memory_tests passes, including the added case that pins the interior-address
    case: with a scan that starts at lower_bound alone the suite aborts at
    GuestMemory.cpp:2009.
  • python3 tools/check_conventions.py --base origin/main → all conventions checks passed.

Measured on the same machine, the real translation unit, 35,000 registered 64 KiB ranges
and 20,000 probes mixed across range starts, interiors and gaps:

operation before after
GuestAllocationsOverlaps ×20,000 1723.6 ms 1.8 ms
GuestAllocationsRequireAvailable ×20,000 1672.9 ms 5.6 ms
GuestAllocationsAcquire ×2,000 982 ms 849 ms

Identical results before and after: 4375 overlaps found, 4375 mappings refused.

No title was run. The win is measured on the registry with the title's range count,
which is the cost the profile attributes to these calls; it is not an end-to-end frame-rate
claim. #4634's own title runs belong to whoever owns that dump.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw; no silent stubs added
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA04263, PPSA04264 Grand Theft Auto V discussion #1203 — reported against this title, not verified on it
  • GPUs tested: n/a (host-side memory registry; no GPU execution in the change)
  • AI-assisted: yes

@github-actions github-actions Bot added the libc label Oct 15, 2026
This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

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.

Pull request Shared files not compared
PR 1835 core/libs/prx/libc/src/GuestAllocations.cpp
PR 4449 core/libs/tests/GuestMemory.cpp
PR 5021 core/libs/prx/libc/src/GuestAllocations.cpp
core/libs/tests/GuestMemory.cpp
PR 6044 core/libs/tests/GuestMemory.cpp
PR 6469 core/libs/tests/GuestMemory.cpp
PR 6565 core/libs/prx/libc/src/GuestAllocations.cpp
core/libs/tests/GuestMemory.cpp
PR 6879 core/libs/prx/libc/src/GuestAllocations.cpp
core/libs/tests/GuestMemory.cpp
PR 7105 core/libs/tests/GuestMemory.cpp
PR 7201 core/libs/prx/libc/src/GuestAllocations.cpp
PR 7631 core/libs/tests/GuestMemory.cpp
PR 7938 core/libs/tests/GuestMemory.cpp
PR 8658 core/libs/prx/libc/src/GuestAllocations.cpp
PR 9011 core/libs/prx/libc/src/GuestAllocations.cpp
core/libs/tests/GuestMemory.cpp
PR 9153 core/libs/tests/GuestMemory.cpp
PR 9181 core/libs/tests/GuestMemory.cpp
PR 9645 core/libs/tests/GuestMemory.cpp
PR 9770 core/libs/tests/GuestMemory.cpp

@simonbronk

Copy link
Copy Markdown
Contributor

Independent Windows CPU validation of head a52b7235b21834d0f8b5541ff2f3b8126f724c1d against PR base 341613ab62b979e429daa1be5ac2fe524a737df6 (AI-assisted).

I compiled each version's complete, unchanged GuestAllocations.cpp with its unchanged GuestAllocations.hpp (header identical between the two refs), plus an external synthetic probe. Windows 11 22631, Intel Core i7-8700, WinLibs GCC 15.2.0 r7, -std=c++20 -O3 -DNDEBUG -static -pthread; no LTO.

Correctness: 177,125 deterministic scenarios per execution, zero mismatches on each of four executions (base, PR, PR, base). Registry sizes 0, 1, 32, 512, 4,096 and 16,384; non-overlapping ranges have 4/8/12/16 KiB lengths on a 32 KiB stride. For each size, 12,000 random valid queries (seed 6321) check both Overlaps and RequireAvailable against a separate brute-force intersection oracle; additional one-byte boundary/interior/gap queries check Overlaps. The registry is held under a Mutation; fake addresses are registered, never dereferenced or OS-mapped.

Timing: these are deliberately worst-position free queries beyond the last registration, excluding setup, mutations and exceptions. Five batches of 3,000 calls per function per registry size per execution; table is the median of ten batches per version, ns/call, base -> PR:

Registered ranges Overlaps RequireAvailable
0 4.82 -> 5.62 4.55 -> 5.60
1 6.10 -> 11.57 7.57 -> 10.22
32 84.93 -> 9.72 89.87 -> 9.60
512 2272.37 -> 17.62 2346.53 -> 17.28
4,096 26888.68 -> 22.08 25137.38 -> 22.37
16,384 97506.80 -> 26.45 99253.72 -> 26.72

The growing-registry result supports the intended removal of a linear prefix scan on Windows too. Empty/single-range registries cost a few more nanoseconds, so this is not a universal speedup. The timing workload is synthetic, cache-warmed, free-only and near the end; it is not a representative frame workload or an FPS result.

Attached: probe source, reproducible Windows runner (set CXX to your compiler), pinned source/header snapshots, machine/compiler flags, raw samples and hashes. No production edit or separate PR.

Limits: no full emulator build, project CTest/GuestMemory suite, hardware GPU/title run, concurrency stress, OS mappings, zero-sized registrations, invalid-input validation, lease-acquire benchmark or composition with current main/other PRs tested. This independently validates the two overlap-query changes, not the whole PR or resolution of #4634 on a title.
windows-perf-pr6321.zip

@simonbronk

Copy link
Copy Markdown
Contributor

Minimal cleanup for the seven current Conventions failures on head a52b7235b21834d0f8b5541ff2f3b8126f724c1d (AI-assisted).

The patch removes only the four prose-comment lines before firstRangeTouching and the three before its regression. The non-comment C++ text is identical after whitespace normalization, checked with the repository's C++ tokenizer; no algorithm, test statement or new debt entry is added.

--- a/core/libs/prx/libc/src/GuestAllocations.cpp
+++ b/core/libs/prx/libc/src/GuestAllocations.cpp
@@ -355,10 +355,6 @@
     }
 }
 
-// The lowest registered range that can touch `address`: the one that starts at or
-// after it, or the one before it when that one reaches into the address. Starting a
-// scan at lower_bound alone misses a range the address falls inside, which is exactly
-// the overlap these queries have to report.
 decltype(registry().ranges)::iterator firstRangeTouching(std::uintptr_t address) {
     auto& ranges = registry().ranges;
     auto it = ranges.lower_bound(address);
--- a/core/libs/tests/GuestMemory.cpp
+++ b/core/libs/tests/GuestMemory.cpp
@@ -2330,9 +2330,6 @@
 }
 #endif
 
-// The overlap queries must start at the range that reaches into the queried address,
-// not at the first range whose base is above it: a query landing inside a registered
-// range is the case these two functions exist to report.
 static void CheckOverlapQueriesSeeRangesStartingBelowTheAddress() {
     constexpr std::size_t page = 0x4000;
     constexpr std::size_t many = 4096;

I reproduced all seven [comment] findings using the trusted tools/check_conventions.py and nid_names.py from main 2e5dd5aa144af1bf90a214a6341bf3e1b4e6d47a, against a minimal isolated Git fixture containing the exact two files from the PR base and head. The same checker against the cleaned fixture exits 0: All conventions checks passed.

This checks the affected file delta, not the complete upstream tree, PR template or hosted CI. No native tests/benchmarks were repeated for this comment-only change; my earlier CPU probe remains evidence only at its recorded head. No source-checkout edit, push to your branch or separate PR. The author still needs to apply the patch and rerun the hosted check.

@coltken95

Copy link
Copy Markdown

Tested on Linux x86-64 (Release build) at a52b723, which merges cleanly with main 03c4cb5:

  • guest_memory passes.
  • tools/check_conventions.py --base main still reports the 7 [comment] errors (GuestAllocations.cpp:358-361, GuestMemory.cpp:2333-2335), so the hosted check stays red until those comments go.

The new test also passes on main, since main returns the same results, just slower. To show the speedup itself, here is a cost test modelled on CheckMappingCostFollowsTheMappingCount. It times both queries against 1024 and 16384 registered ranges and requires the large case to cost at most 4x the small one:

  • With this PR: ~0.10 ms vs ~0.13 ms (about 1.3x), passing in 5 of 5 runs.
  • With main's GuestAllocations.cpp: 0.027 s vs 0.97-1.02 s (about 35x), so it fails at Require(large <= small * 4).

The patch below also removes the 7 comments, so git apply on the PR head should clear both.

Patch against a52b723
diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp
index a6ec3cd96..824cfdec3 100644
--- a/core/libs/prx/libc/src/GuestAllocations.cpp
+++ b/core/libs/prx/libc/src/GuestAllocations.cpp
@@ -355,10 +355,6 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi
     }
 }
 
-// The lowest registered range that can touch `address`: the one that starts at or
-// after it, or the one before it when that one reaches into the address. Starting a
-// scan at lower_bound alone misses a range the address falls inside, which is exactly
-// the overlap these queries have to report.
 decltype(registry().ranges)::iterator firstRangeTouching(std::uintptr_t address) {
     auto& ranges = registry().ranges;
     auto it = ranges.lower_bound(address);
diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp
index eb5ba5948..cd2ae57ac 100644
--- a/core/libs/tests/GuestMemory.cpp
+++ b/core/libs/tests/GuestMemory.cpp
@@ -2328,11 +2328,46 @@ static void CheckMappingCostFollowsTheMappingCount() {
     Require(large <= small * 48);
     Require(sceKernelReleaseDirectMemory(phys, page * many) == 0);
 }
+
+static double OverlapQueriesSeconds(std::size_t ranges) {
+    constexpr std::size_t page = 0x4000;
+    constexpr std::size_t queries = 2000;
+    constexpr std::uintptr_t first = 0x7e0000000000ull;
+    for (std::size_t index = 0; index < ranges; ++index) {
+        GuestAllocations::Mutation mutation;
+        mutation.Add(reinterpret_cast<void*>(first + page * 2 * index), page, true, true, false);
+    }
+    const auto gap = reinterpret_cast<const void*>(first + page * 2 * (ranges - 1) + page);
+    const auto threadSeconds = [] {
+        timespec now{};
+        Require(clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now) == 0);
+        return static_cast<double>(now.tv_sec) + static_cast<double>(now.tv_nsec) * 1e-9;
+    };
+    double best = 1e9;
+    for (int attempt = 0; attempt < 3; ++attempt) {
+        const auto before = threadSeconds();
+        for (std::size_t query = 0; query < queries; ++query) {
+            Require(!GuestAllocations::GuestAllocationsOverlaps_nid_postfix(nullptr, gap, page));
+            GuestAllocations::GuestAllocationsRequireAvailable_nid_postfix(nullptr, gap, page);
+        }
+        best = std::min(best, threadSeconds() - before);
+    }
+    for (std::size_t index = 0; index < ranges; ++index) {
+        GuestAllocations::Mutation mutation;
+        mutation.Remove(reinterpret_cast<const void*>(first + page * 2 * index));
+    }
+    return best;
+}
+
+static void CheckOverlapQueryCostFollowsTheTouchedRanges() {
+    constexpr std::size_t few = 1024;
+    constexpr std::size_t many = few * 16;
+    const auto small = OverlapQueriesSeconds(few);
+    const auto large = OverlapQueriesSeconds(many);
+    Require(large <= small * 4);
+}
 #endif
 
-// The overlap queries must start at the range that reaches into the queried address,
-// not at the first range whose base is above it: a query landing inside a registered
-// range is the case these two functions exist to report.
 static void CheckOverlapQueriesSeeRangesStartingBelowTheAddress() {
     constexpr std::size_t page = 0x4000;
     constexpr std::size_t many = 4096;
@@ -2657,6 +2692,7 @@ int main(int argc, char** argv) {
     CheckDirectMemoryWriteWatch();
     CheckDirectMemoryWriteWatchFollowsRemovedMappings();
     CheckMappingCostFollowsTheMappingCount();
+    CheckOverlapQueryCostFollowsTheTouchedRanges();
     CheckDirectMemoryBackingNeedsNoFilesystem();
     CheckDirectMemorySharedBacking();
     CheckVirtualQueryFindsNeighbours();

AI-assisted: yes (Claude Code)

m2 (AI Agent) and others added 2 commits October 19, 2026 01:01
…hat touches the address

GuestAllocationsOverlaps and GuestAllocationsRequireAvailable walked the whole
range registry from begin(), so every mprotect-sized query paid for the
registrations below it. A title that keeps tens of thousands of live 64 KiB
direct mappings turns that into queue-worker time: boykopovar#4634's samples put the
wait in exactly these two calls.

Start both at the lowest range that can touch the queried address: the first
one at or after it, or the one before it when that one reaches into the
address. A query landing inside a registered range is the case these
functions exist to report, so the step back is what keeps the result
identical.

Measured on this host with 35,000 registered 64 KiB ranges and 20,000 probes
mixed across range starts, interiors and gaps: Overlaps 1723.6 ms -> 1.8 ms,
RequireAvailable 1672.9 ms -> 5.6 ms, same hit counts (4375 each). Acquire
gains a reserve() over the registry size: 982 ms -> 849 ms for 2,000 leases.

The added case pins the interior-address case; it fails on a scan that starts
at lower_bound alone.
@adriadam10
adriadam10 force-pushed the wip/gta-v-4634-registry-scan branch from a52b723 to 0533ecc Compare October 18, 2026 23:04
@github-actions

Copy link
Copy Markdown

This pull request is labeled perf, so it needs the summary line of a capture on main and one with the change, from the same title, scene and machine, as the Performance report form asks. Please paste both aps5-summary.jsonl lines, attach the two traces and add the output of tools/trace_digest.py.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@JJenkx

JJenkx commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

AI-assisted independent validation of a local refresh of this PR on main 252cbc29beac26811ad9338c22bcba336d7d2ebe (candidate 0e448213e27e581a97e3031bb1215a4e0054b018).

The attached refresh.patch preserves both original authors. Resolving the GuestMemory.cpp conflict keeps both main's regular-file-mapping test and this PR's overlap regression. Main's current strong readableLease cache remains intact. libs and guest_memory_tests build; guest_memory passes on main and candidate; conventions and git diff --check pass.

PPSA05347, Record of Lodoss War: Deedlit in Wonder Labyrinth, forest sword/jump tutorial: 26.068 FPS main → 27.946 FPS candidate (+7.205% observed). Frame p50/p90/p99: main 36.525/46.630/59.935 ms; candidate 34.471/41.212/54.146 ms. Both reached the same tutorial location in the 190/240-second screenshots and exited cleanly.

Ryzen 9 7950X / RX 6650 XT, RADV Mesa 26.2.4, Linux 7.2.9; trace resolution 3840×2160. One candidate-then-main pair, identical starting warm-cache clones, same scripted inputs, pipelined draws and DMA enabled on both, trace window seconds 200:30. This is preliminary evidence, not a replicated or general speed claim. The current comparison tool flags window.first_present (9518 main / 9793 candidate); the fixed wall-clock capture interval, GPU/driver/OS/resolution and recorded environment match. No metadata was rewritten to bypass that check. Neither trace reports dropped events. Screenshot checks do not establish full-game correctness.

Evidence archive includes both complete traces, original summary lines, tools/trace_digest.py --json outputs, comparison output, screenshots, launch protocols/cache manifest, focused test logs, and refresh patch. SHA-256: 76946d0adae3437e3f349326bdba194de22c568640c8c17660a019a097b9298a.

Main summary:

{"commit": "252cbc29beac", "dirty": false, "env": {"APS5_DRAW_CACHE_ENTRIES": "4096", "APS5_HOST_IMPORT_MIB": "16384", "APS5_PAD_INPUT_FILE": "<path>", "APS5_PIPELINED_DRAWS": "1", "APS5_PIPELINE_DMA": "1", "APS5_SPIRV_OPT": "full", "APS5_TRACE_SECONDS": "200:30"}, "gpu": "AMD Radeon RX 6650 XT (RADV NAVI23)", "driver": "radv Mesa 26.2.4-arch1.1", "os": "Linux 7.2.9-arch1-1", "resolution": "3840x2160", "window": {"spec": "seconds 200:30", "first_present": 9518, "ms": 30036.52}, "presents": 783, "frame_ms": {"p50": 36.525, "p90": 46.63, "p99": 59.935, "max": 75.545}, "gpu_busy_ms_per_present": 2.195, "worker_busy_pct": {"queue worker 0x0": 29.237}, "top_phases_ms_per_frame": {"Driver.Packet": 11.205, "Graphics.Draw": 5.481, "Graphics.Draw.validate": 3.77, "Recorder.Sync": 1.926, "Graphics.Draw.draw_and_resource_release": 1.699, "VideoOut.Flip": 1.658, "Driver.Present": 1.622, "VideoOut.Flip.present": 1.622}, "trace_dropped": false, "trace": "aps5-trace-252cbc29beac-995367.json.gz"}

Candidate summary:

{"commit": "0e448213e27e", "dirty": false, "env": {"APS5_DRAW_CACHE_ENTRIES": "4096", "APS5_HOST_IMPORT_MIB": "16384", "APS5_PAD_INPUT_FILE": "<path>", "APS5_PIPELINED_DRAWS": "1", "APS5_PIPELINE_DMA": "1", "APS5_SPIRV_OPT": "full", "APS5_TRACE_SECONDS": "200:30"}, "gpu": "AMD Radeon RX 6650 XT (RADV NAVI23)", "driver": "radv Mesa 26.2.4-arch1.1", "os": "Linux 7.2.9-arch1-1", "resolution": "3840x2160", "window": {"spec": "seconds 200:30", "first_present": 9793, "ms": 30021.747}, "presents": 839, "frame_ms": {"p50": 34.471, "p90": 41.212, "p99": 54.146, "max": 63.89}, "gpu_busy_ms_per_present": 2.189, "worker_busy_pct": {"queue worker 0x0": 28.939}, "top_phases_ms_per_frame": {"Driver.Packet": 10.344, "Graphics.Draw": 5.025, "Graphics.Draw.validate": 3.373, "VideoOut.Flip": 1.696, "Recorder.Sync": 1.671, "VideoOut.Flip.present": 1.66, "Driver.Present": 1.66, "Graphics.Draw.draw_and_resource_release": 1.64}, "trace_dropped": false, "trace": "aps5-trace-0e448213e27e-900224.json.gz"}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

libc needs rebase Conflicts with main, rebase needed perf numbers missing perf pull request without a before and after summary line perf Performance change unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants