perf(libc): size each write-watch query to the region it covers

WindowsMappings::Collect handed GetWriteWatch the whole remaining output
capacity for every private region it walked. The call's cost follows that
capacity, not the size of the range: on Windows 11 (26200) a query of one
16 KiB region took 0.35 us with room for 4 entries and 6.5 us with room
for the 65536 the write tracker passes. Flexible memory is committed in
16 KiB allocations, one region each, so a resetting walk over 33 MiB of it
took 14.7 ms, 5.8 times a memcmp of the same bytes.

Each query now gets room for the pages its region spans, which is the most
it can report, so every walk returns the same pages; the 33 MiB walk takes
1.23 ms. guest_memory checks that a walk over eight 16 KiB allocations
reports every stored page once, in order, with room for 32, 5 and 1 pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Niko 2026-10-13 01:22:35 +02:00
parent 2f969f147b
commit 44767a7b97
2 changed files with 41 additions and 2 deletions

View File

@ -346,8 +346,9 @@ public:
const auto memory = query(cursor);
if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false;
const auto stop = std::min(end, reinterpret_cast<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize);
ULONG_PTR available = capacity - *count;
if (available == 0) return true;
if (capacity == *count) return true;
const auto touched = (((stop + 4095) & ~static_cast<std::uintptr_t>(4095)) - (cursor & ~static_cast<std::uintptr_t>(4095))) / 4096;
ULONG_PTR available = std::min<ULONG_PTR>(capacity - *count, touched);
DWORD granularity = 0;
if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast<void*>(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes");
*count += available;

View File

@ -1492,6 +1492,43 @@ static void CheckFailedCollectKeepsWrites() {
#endif
}
static void CheckSplitPrivateCollect() {
#ifdef _WIN32
constexpr std::size_t granule = 0x4000;
constexpr std::size_t regions = 8;
constexpr std::size_t bytes = granule * regions;
constexpr std::size_t pages = bytes / 4096;
auto* block = static_cast<unsigned char*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, granule));
GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, granule);
const auto address = reinterpret_cast<std::uintptr_t>(block);
const auto collect = [&](std::size_t capacity) {
std::vector<std::uintptr_t> found;
std::array<void*, pages> buffer{};
for (auto cursor = address; cursor < address + bytes;) {
std::size_t count = capacity;
Require(GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, address + bytes - cursor, buffer.data(), &count, true));
for (std::size_t i = 0; i < count; ++i) found.push_back(reinterpret_cast<std::uintptr_t>(buffer[i]));
if (count < capacity) break;
cursor = found.back() + 4096;
}
return found;
};
collect(pages);
for (const std::size_t capacity : {pages, std::size_t{5}, std::size_t{1}}) {
for (std::size_t page = 0; page < pages; ++page) static_cast<volatile unsigned char*>(block)[page * 4096 + 7] = static_cast<unsigned char>(capacity + page);
const auto found = collect(capacity);
Require(found.size() == pages);
for (std::size_t page = 0; page < pages; ++page) Require(found[page] == address + page * 4096);
Require(collect(pages).empty());
}
static_cast<volatile unsigned char*>(block)[granule * 3 + 4096 + 1] = 9;
const auto one = collect(pages);
Require(one.size() == 1 && one[0] == address + granule * 3 + 4096);
GuestArena::GuestArenaReset_nid_postfix(block, bytes);
GuestArena::GuestArenaRelease_nid_postfix(block, bytes);
#endif
}
static void CheckPinnedSharedPages() {
#ifdef _WIN32
constexpr std::size_t page = 0x4000;
@ -2156,6 +2193,7 @@ int main() {
CheckFixedMappingReplacesPartialOverlap();
CheckFixedMappingOverHoleInsideAllocation();
CheckDirectMemoryGpuProtBits();
CheckSplitPrivateCollect();
CheckGpuAccessFollowsProtection();
CheckGpuMapsAreObserved();
CheckClearingTheObserverWaitsForItsCalls();