Skip to content

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

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

mariuszkreft wants to merge 1 commit 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

…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.
@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

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.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6542 core/libs/tests/GuestMemory.cpp L2467

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 1324 core/libs/prx/libc/src/GuestAllocations.cpp
PR 1835 core/libs/prx/libc/src/GuestAllocations.cpp
PR 5021 core/libs/prx/libc/src/GuestAllocations.cpp
core/libs/tests/GuestMemory.cpp
PR 6044 core/libs/tests/GuestMemory.cpp
PR 6499 core/libs/prx/libc/src/GuestAllocations.cpp
PR 6565 core/libs/prx/libc/src/GuestAllocations.cpp
core/libs/tests/GuestMemory.cpp
PR 6879 core/libs/prx/libc/src/GuestAllocations.cpp
PR 7105 core/libs/tests/GuestMemory.cpp
PR 7201 core/libs/prx/libc/src/GuestAllocations.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)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants