diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 7d10d9716ebd..d9566ed0f261 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -388,6 +388,14 @@ if(BUILD_TESTING) endif() add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) + add_test_executable(windows_shared_map_cost_tests tests/WindowsSharedMapCost.cpp) + target_include_directories(windows_shared_map_cost_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(windows_shared_map_cost_tests PRIVATE _WIN32_WINNT=0x0A00) + if(MINGW) + target_compile_options(windows_shared_map_cost_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_shared_map_cost COMMAND windows_shared_map_cost_tests) + set_tests_properties(windows_shared_map_cost PROPERTIES TIMEOUT 120) endif() if(WIN32) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 9e348301a8d2..d27d51c84f13 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -136,6 +136,8 @@ class WindowsMappings { const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; + const bool perPage = !resident && sectionBytes == SIZE_MAX; + if (perPage) commitSection(section, offset, bytes); for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; if (single(done)) { @@ -165,23 +167,56 @@ class WindowsMappings { done += runBytes; continue; } - const auto slot = slotOf(done); - const auto shared = pageOf(*slot); - const auto aliases = shared->aliases; - for (const auto alias : aliases) { - if (views.at(alias).resident) demote(alias); - views.at(alias).aliasDemoted = false; + if (sectionBytes != SIZE_MAX) { + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + views.at(alias).aliasDemoted = false; + } + const auto viewBytes = std::min(pageBytes, sectionBytes - done); + split(base, pageBytes); + mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + slot->stored = true; + shared->aliases.push_back(base); + const auto created = views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}).first; + if (shared->comparedWhenShared) created->second.unreported = allSubpages; + remember(freshRanges, base, base + pageBytes); + invalidate(*shared); + done += pageBytes; + continue; + } + std::vector, std::shared_ptr>> pages; + pages.reserve((bytes - done) / pageBytes); + auto runBytes = std::size_t{0}; + do { + const auto slot = slotOf(done + runBytes); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + views.at(alias).aliasDemoted = false; + } + pages.emplace_back(slot, shared); + runBytes += pageBytes; + } while (done + runBytes < bytes && !single(done + runBytes)); + if (!perPage) commitSection(section, offset + done, runBytes); + split(base, runBytes); + mapRun(base, runBytes, section, offset + done); + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect shared guest pages"); + collectableRanges.clear(); + for (std::size_t at = 0; at < runBytes; at += pageBytes) { + auto& [slot, shared] = pages[at / pageBytes]; + slot->stored = true; + shared->aliases.push_back(base + at); + invalidate(*shared); + const auto created = views.emplace(base + at, View{std::move(shared), protection, {}, 0, owned, offset + done + at, 0, 0, false, std::move(slot)}).first; + if (created->second.page->comparedWhenShared) created->second.unreported = allSubpages; } - const auto viewBytes = std::min(pageBytes, sectionBytes - done); - split(base, pageBytes); - mapShared(base, section, offset + done, protection, viewProtection, viewBytes); - slot->stored = true; - shared->aliases.push_back(base); - const auto created = views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}).first; - if (shared->comparedWhenShared) created->second.unreported = allSubpages; - remember(freshRanges, base, base + pageBytes); - invalidate(*shared); - done += pageBytes; + remember(freshRanges, base, base + runBytes); + done += runBytes; } } @@ -771,12 +806,23 @@ class WindowsMappings { return (bytes + subpageBytes - 1) & ~(subpageBytes - 1); } - void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes) { + void commitSection(HANDLE section, std::uint64_t offset, std::size_t bytes) { + auto window = open(section, offset, bytes); + const auto memory = query(reinterpret_cast(window.bytes)); + if ((memory.State != MEM_COMMIT || memory.RegionSize < bytes) && !VirtualAlloc(window.bytes, bytes, MEM_COMMIT, PAGE_READWRITE)) { + const auto error = GetLastError(); + close(window); + throw std::system_error(static_cast(error), std::system_category(), "commit direct memory pages"); + } + close(window); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes, bool committed = false) { void* page = reinterpret_cast(base); const auto viewBytes = hostPages(sectionBytes); if (viewBytes != pageBytes) split(base, viewBytes); if (!map(section, GetCurrentProcess(), page, offset, sectionBytes, MEM_REPLACE_PLACEHOLDER, viewProtection, nullptr, 0)) fail("map shared guest page"); - if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + if (!committed && query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, viewBytes, protection, &previous)) fail("protect shared guest page"); if (viewBytes == pageBytes) return; @@ -785,6 +831,47 @@ class WindowsMappings { if (!VirtualProtect(zeros, pageBytes - viewBytes, protection, &previous)) fail("protect guest pages past the end of a file"); } + void mapRun(std::uintptr_t base, std::size_t bytes, HANDLE section, std::uint64_t offset) { + if (!map(section, GetCurrentProcess(), reinterpret_cast(base), offset, bytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map a run of shared guest pages"); + } + + void breakView(std::uintptr_t at) { + const auto memory = query(at); + if (memory.State == MEM_FREE || memory.Type != MEM_MAPPED) return; + const auto start = reinterpret_cast(memory.AllocationBase); + if (start == at) return; + const auto first = views.find(start); + if (first == views.end()) return; + struct Part { + std::uintptr_t base; + std::uintptr_t end; + DWORD protection; + bool committed; + }; + std::vector parts; + auto stop = start; + for (;;) { + const auto part = query(stop); + if (part.AllocationBase != memory.AllocationBase) break; + const auto partEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + parts.push_back({stop, partEnd, part.Protect, part.State == MEM_COMMIT}); + stop = partEnd; + } + const auto section = first->second.section; + const auto offset = first->second.offset; + if (!unmap(GetCurrentProcess(), reinterpret_cast(start), MEM_PRESERVE_PLACEHOLDER)) fail("unmap a run of shared guest pages to split it"); + split(start, at - start); + mapRun(start, at - start, section->handle, offset); + mapRun(at, stop - at, section->handle, offset + (at - start)); + for (const auto& part : parts) { + if (!part.committed) continue; + for (const auto [from, to] : {std::pair{part.base, std::min(part.end, at)}, std::pair{std::max(part.base, at), part.end}}) { + DWORD previous; + if (from < to && !VirtualProtect(reinterpret_cast(from), to - from, part.protection, &previous)) fail("protect a split run of shared guest pages"); + } + } + } + static bool blank(std::uintptr_t page) { const auto* words = reinterpret_cast(page); for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { @@ -976,7 +1063,7 @@ class WindowsMappings { return memory; } - static void split(std::uintptr_t address, std::size_t bytes) { + static std::uintptr_t split(std::uintptr_t address, std::size_t bytes) { auto memory = query(address); memory = query(reinterpret_cast(memory.AllocationBase)); if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder"); @@ -987,6 +1074,7 @@ class WindowsMappings { } if (memory.RegionSize < bytes) throw std::runtime_error("guest placeholder is too small"); if (memory.RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + return address + memory.RegionSize; } void reset(std::uintptr_t address, std::size_t bytes) { @@ -995,6 +1083,8 @@ class WindowsMappings { forgetClean(address, end); breakRun(address); breakRun(end); + breakView(address); + breakView(end); if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { const auto memory = query(address); if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); @@ -1026,12 +1116,11 @@ class WindowsMappings { if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); if (memory.Type == MEM_MAPPED) { if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); - const auto found = views.find(cursor); - if (found != views.end()) { - std::erase(found->second.page->aliases, cursor); - views.erase(found); - forget(freshRanges, cursor, cursor + pageBytes); + for (auto found = views.lower_bound(cursor); found != views.end() && found->first < allocationEnd;) { + std::erase(found->second.page->aliases, found->first); + found = views.erase(found); } + forget(freshRanges, cursor, allocationEnd); } else if (memory.Type == MEM_PRIVATE) { if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); diff --git a/core/libs/tests/WindowsSharedMapCost.cpp b/core/libs/tests/WindowsSharedMapCost.cpp new file mode 100644 index 000000000000..ce6c25b6e7ed --- /dev/null +++ b/core/libs/tests/WindowsSharedMapCost.cpp @@ -0,0 +1,78 @@ +#define NOMINMAX +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +using Mappings = GuestArena::WindowsMappings; +constexpr std::size_t PageBytes = 16384; +constexpr int Rounds = 5; + +[[noreturn]] void Fail(const char* what) { + std::fprintf(stderr, "windows shared map cost: %s (error %lu)\n", what, GetLastError()); + std::exit(1); +} + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(cursor, &info, sizeof(info)) != sizeof(info)) Fail("view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +struct Sample { + double mapMs = 0; + double releaseMs = 0; + std::size_t views = 0; +}; + +Sample MapOnce(std::size_t pages) { + const auto bytes = pages * PageBytes; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(static_cast(bytes) >> 32), static_cast(bytes), nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, bytes)); + if (section == nullptr || memory == nullptr) Fail("cannot allocate shared memory"); + const auto start = std::chrono::steady_clock::now(); + Mappings::Get().Map(memory, bytes, section, 0, PAGE_READWRITE); + Sample sample; + sample.mapMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + sample.views = MappedViews(memory, bytes); + memory[0] = 1; + memory[bytes - 1] = 1; + const auto released = std::chrono::steady_clock::now(); + Mappings::Get().Reset(memory, bytes); + sample.releaseMs = std::chrono::duration(std::chrono::steady_clock::now() - released).count(); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + return sample; +} + +double Median(std::vector values) { + std::sort(values.begin(), values.end()); + return values[values.size() / 2]; +} + +} + +int main() { + for (const std::size_t pages : {std::size_t{256}, std::size_t{16384}, std::size_t{65536}}) { + std::vector map; + std::vector release; + std::size_t views = 0; + for (int round = 0; round < Rounds; ++round) { + const auto sample = MapOnce(pages); + map.push_back(sample.mapMs); + release.push_back(sample.releaseMs); + views = sample.views; + } + std::printf("shared map of %zu pages (%zu MiB): map median %.2f ms (%.2f us per page), release median %.2f ms, %zu views\n", pages, pages * PageBytes >> 20, Median(map), Median(map) * 1000.0 / static_cast(pages), Median(release), views); + } + return 0; +} diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 7a42cdea832b..65d98f68e2da 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -535,6 +535,103 @@ void CheckComparedPagesReportedOnce() { } } +namespace { +void CheckSharedMapQueriesPerMapping() { + constexpr std::size_t Pages = 256; + constexpr std::size_t Span = Pages * 16384; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + Require(section && memory, "cannot allocate shared test memory"); + const auto before = Mappings::MemoryQueries(); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + const auto queries = Mappings::MemoryQueries() - before; + for (std::size_t page = 0; page < Pages; ++page) memory[page * 16384 + 8] = static_cast(page); + for (std::size_t page = 0; page < Pages; ++page) Require(memory[page * 16384 + 8] == static_cast(page), "a shared page lost its store"); + Mappings::Get().Reset(memory, Span); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + Require(queries < Pages, ("a shared mapping queried the address space for every page: " + std::to_string(queries) + " queries for " + std::to_string(Pages) + " pages").c_str()); +} + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info), "view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +DWORD ProtectionAt(const unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "protection query failed"); + return info.State == MEM_COMMIT ? info.Protect : 0; +} + +void CheckSharedRunIsOneView() { + constexpr std::size_t Gib = std::size_t{1} << 30; + HANDLE large = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, static_cast(Gib), nullptr); + auto* whole = static_cast(Mappings::Get().Reserve(nullptr, Gib)); + Require(large && whole, "cannot allocate a shared gibibyte"); + Mappings::Get().Map(whole, Gib, large, 0, PAGE_READWRITE); + const auto wholeViews = MappedViews(whole, Gib); + whole[Gib - 1] = 1; + Mappings::Get().Reset(whole, Gib); + VirtualFree(whole, 0, MEM_RELEASE); + CloseHandle(large); + Require(wholeViews == 1, ("a shared gibibyte was mapped as " + std::to_string(wholeViews) + " views instead of one").c_str()); + + constexpr std::size_t Page = 16384; + constexpr std::size_t Pages = 64; + constexpr std::size_t Span = Pages * Page; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span + Page, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + auto* other = static_cast(Mappings::Get().Reserve(nullptr, Page)); + Require(section && memory && other, "cannot allocate shared test memory"); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + for (std::size_t page = 0; page < Pages; ++page) memory[page * Page + 8] = static_cast(page + 1); + Require(MappedViews(memory, Span) == 1, "a shared run was not mapped as one view"); + Mappings::Get().SetProtection(reinterpret_cast(memory + 5 * Page), Page, PAGE_READONLY); + DWORD previous; + Require(VirtualProtect(memory + 5 * Page, Page, PAGE_READONLY, &previous), "guest protection of one page failed"); + Require(MappedViews(memory, Span) == 1, "a per-page protection split the run"); + Mappings::Get().Map(other, Page, section, 30 * Page, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 1, "an alias of one page split the run"); + other[16] = 0x5a; + Require(memory[30 * Page + 16] == 0x5a, "an alias store did not reach the run"); + std::array collected{}; + std::size_t available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), Page, collected.data(), &available, true, Collection::All), "run collection failed"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "collection did not arm the run's page"); + Mappings::Get().Reset(memory + 10 * Page, Page); + Require(MappedViews(memory, Span) == 2, ("a partial unmap left " + std::to_string(MappedViews(memory, Span)) + " views instead of two").c_str()); + Require(ProtectionAt(memory + 10 * Page) == 0, "the unmapped page is still mapped"); + Mappings::Get().Map(memory + 20 * Page, Page, section, Span, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 4, ("a remap of one page left " + std::to_string(MappedViews(memory, Span)) + " views instead of four").c_str()); + Require(memory[20 * Page + 8] == 0, "the remapped page shows the old page"); + for (std::size_t page = 0; page < Pages; ++page) { + if (page == 10 || page == 20) continue; + Require(memory[page * Page + 8] == static_cast(page + 1), ("a split lost page " + std::to_string(page)).c_str()); + } + Require(ProtectionAt(memory + 5 * Page) == PAGE_READONLY && ProtectionAt(memory + 6 * Page) == PAGE_READWRITE, "a split lost a page protection"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "a split dropped a write-tracking protection"); + const auto before = faults.load(); + memory[50 * Page + 8] = 0x77; + Require(faults == before + 1, "a write to an armed page of a split run did not fault"); + memory[51 * Page + 8] = 0x78; + available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), 2 * Page, collected.data(), &available, true, Collection::Armed), "armed collection failed"); + Require(available == 1 && collected[0] == memory + 50 * Page, "the split run lost a tracked write"); + Mappings::Get().Reset(memory, Span); + Mappings::Get().Reset(other, Page); + VirtualFree(memory, 0, MEM_RELEASE); + VirtualFree(other, 0, MEM_RELEASE); + CloseHandle(section); +} +} + int main() { auto* handler = AddVectoredExceptionHandler(1, HandleWrite); try { @@ -547,6 +644,8 @@ int main() { CheckPrivate(); CheckConcurrentWrites(); CheckForgottenSectionKeepsNoPages(); + CheckSharedMapQueriesPerMapping(); + CheckSharedRunIsOneView(); CheckComparedWhenShared(); CheckResidentOnlyComparedWhenShared(); CheckComparedWhenSharedRemapped();