[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.
This commit is contained in:
kuba 2026-09-26 13:19:48 +02:00 committed by GitHub
parent ac8ffc3f3a
commit 155add2c10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 78 additions and 4 deletions

View File

@ -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;

View File

@ -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(

View File

@ -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;

View File

@ -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<Region>(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)

View File

@ -16,6 +16,14 @@ namespace SharpEmu.HLE;
/// </summary>
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;