From 155add2c10675c487e20a4c6551d802f787a504f Mon Sep 17 00:00:00 2001 From: kuba Date: Sat, 26 Sep 2026 13:19:48 +0200 Subject: [PATCH] [CPU] Cache readable stack ranges per thread for import arguments on POSIX (#911) Every import reads its stack arguments through a region query, which on POSIX takes the global region lock and scans the view regions. With many guest threads this was the most contended lock in the process (about 36k contentions per second in Poppy Playtime) and HLE calls averaged ~59 us. Keep the last readable range per thread for the import path and reuse it until any POSIX mapping or protection changes. The signal handlers keep the uncached query, since they must not touch thread-static storage. --- .../DirectExecutionBackend.Diagnostics.cs | 23 +++++++++++ .../Native/DirectExecutionBackend.Imports.cs | 41 +++++++++++++++++-- .../Host/Posix/PosixHostMemory.cs | 5 ++- .../Host/Posix/PosixViewRegions.cs | 2 + src/SharpEmu.HLE/HostMemory.cs | 11 +++++ 5 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs index f226f3b..059ec9d 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs @@ -463,6 +463,29 @@ public sealed partial class DirectExecutionBackend return false; } + // The committed readable run of host pages that contains address. + private unsafe static bool TryQueryReadableRange(ulong address, out ulong start, out ulong end) + { + start = 0; + end = 0; + if (address <= 65536 || address >= 140737488355328L || + VirtualQuery((void*)address, out var info, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0) + { + return false; + } + + var regionEnd = info.BaseAddress + info.RegionSize; + if (regionEnd < info.BaseAddress || address > regionEnd - 8 || + info.State != 4096 || !IsReadableProtection(info.Protect)) + { + return false; + } + + start = info.BaseAddress; + end = regionEnd; + return true; + } + private unsafe static bool TryReadStackU64(ulong address, out ulong value) { value = 0uL; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 0456569..dc4ef3a 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -238,8 +238,8 @@ public sealed partial class DirectExecutionBackend if (probeTarget && Interlocked.Increment(ref _probeImportReturnAddressCount) <= 2048) { - var frameValue = TryReadStackU64(value4, out var savedRbp) ? savedRbp : 0; - var frameReturn = TryReadStackU64(value4 + sizeof(ulong), out var savedReturn) + var frameValue = TryReadImportStackU64(value4, out var savedRbp) ? savedRbp : 0; + var frameReturn = TryReadImportStackU64(value4 + sizeof(ulong), out var savedReturn) ? savedReturn : 0; Console.Error.WriteLine( @@ -1217,10 +1217,45 @@ public sealed partial class DirectExecutionBackend } } + // Every import reads its stack arguments. On POSIX a region query takes the global region lock, + // so keep the last readable range per thread until any mapping changes. The signal handlers + // use TryReadStackU64 instead: they must not touch thread-static storage. + [ThreadStatic] private static ulong _importReadableStart; + [ThreadStatic] private static ulong _importReadableEnd; + [ThreadStatic] private static long _importReadableGeneration; + + private unsafe static bool TryReadImportStackU64(ulong address, out ulong value) + { + if (OperatingSystem.IsWindows()) + { + return TryReadHostQword(address, out value); + } + + var generation = HostMemory.MappingGeneration; + if (generation == _importReadableGeneration && + address >= _importReadableStart && address <= _importReadableEnd - 8) + { + value = *(ulong*)address; + return true; + } + + if (!TryQueryReadableRange(address, out var start, out var end)) + { + value = 0; + return false; + } + + _importReadableStart = start; + _importReadableEnd = end; + _importReadableGeneration = generation; + value = *(ulong*)address; + return true; + } + private static ulong ReadImportStackArgument(nint argPackPtr, int index) { var address = checked((ulong)argPackPtr + 104UL + (ulong)index * sizeof(ulong)); - return TryReadHostQword(address, out var value) ? value : 0; + return TryReadImportStackU64(address, out var value) ? value : 0; } private static GuestCpuContinuation CaptureImportBoundaryContinuation( diff --git a/src/SharpEmu.HLE/Host/Posix/PosixHostMemory.cs b/src/SharpEmu.HLE/Host/Posix/PosixHostMemory.cs index 82504d6..f427e67 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixHostMemory.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixHostMemory.cs @@ -328,6 +328,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory } } + HostMemory.OnMappingChanged(); Regions[(ulong)result] = new Region { Base = (ulong)result, @@ -357,7 +358,8 @@ internal sealed unsafe class PosixHostMemory : IHostMemory : munmap((nint)address, (nuint)region.Size) == 0; if (released) { - Regions.Remove((ulong)address); + HostMemory.OnMappingChanged(); + Regions.Remove((ulong)address); } return released; @@ -492,6 +494,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect) { + HostMemory.OnMappingChanged(); if (start == region.Base && size >= region.Size) { region.DefaultProtect = protect; diff --git a/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs b/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs index b238eed..24869ab 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs @@ -13,6 +13,7 @@ internal static class PosixViewRegions internal static void Replace(ulong address, ulong size, uint state, uint protection) { + HostMemory.OnMappingChanged(); var end = RoundPageEnd(address, size); var replacement = new List(Regions.Count + 2); foreach (var region in Regions) @@ -50,6 +51,7 @@ internal static class PosixViewRegions // Mapping creates the storage. Fault-time protection updates must not allocate. internal static void ChangeProtection(ulong address, ulong size, uint protection) { + HostMemory.OnMappingChanged(); var end = RoundPageEnd(address, size); var pageSize = (ulong)Environment.SystemPageSize; foreach (var region in Regions) diff --git a/src/SharpEmu.HLE/HostMemory.cs b/src/SharpEmu.HLE/HostMemory.cs index 4c07442..6586bf5 100644 --- a/src/SharpEmu.HLE/HostMemory.cs +++ b/src/SharpEmu.HLE/HostMemory.cs @@ -16,6 +16,14 @@ namespace SharpEmu.HLE; /// public static unsafe class HostMemory { + private static long _mappingGeneration; + + // Changes whenever a POSIX mapping or its protection changes, so callers can cache a + // query result until the address space changes. + public static long MappingGeneration => Volatile.Read(ref _mappingGeneration); + + internal static void OnMappingChanged() => Interlocked.Increment(ref _mappingGeneration); + public const uint MEM_COMMIT = 0x1000; public const uint MEM_RESERVE = 0x2000; public const uint MEM_RELEASE = 0x8000; @@ -254,6 +262,7 @@ public static unsafe class HostMemory } } + HostMemory.OnMappingChanged(); Regions[(ulong)result] = new Region { Base = (ulong)result, @@ -277,6 +286,7 @@ public static unsafe class HostMemory return false; } + HostMemory.OnMappingChanged(); Regions.Remove((ulong)address); return munmap((nint)address, (nuint)region.Size) == 0; } @@ -439,6 +449,7 @@ public static unsafe class HostMemory private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect) { + HostMemory.OnMappingChanged(); if (start == region.Base && size >= region.Size) { region.DefaultProtect = protect;