[CPU] Run libc memcpy/memmove as a native guest-side stub

Guest memcpy went through the managed gateway on every call: two write
tracking notifications, the memory reader lock and two region lookups, about
0.8 us per call under Rosetta even for a copy of a few bytes. Dreaming Sarah
makes roughly 45k of those calls a second.

Guest memory is identity-mapped, and every fault a copy can take -- write
tracking, lazy commit -- is resolved from the fault address alone, so the copy
can run as a forward rep movsb in a fast-path stub. A destination that sits
inside the source, where a forward copy would read bytes it has already
written, and the write-watch diagnostic both keep the managed export.
This commit is contained in:
kuba 2026-09-28 18:16:28 +02:00
parent 6db42fe981
commit 5fb1cbacdc
3 changed files with 141 additions and 1 deletions

View File

@ -52,6 +52,7 @@ These variables change how the emulator operates.
| --- | --- | --- | --- |
| `SHARPEMU_AGC_VERSION13_DEFAULTS` | text (`legacy`, `generic` or `0`) | Selects the register defaults for AGC version 13 requests. The default uses the version 11 compact tables. The values `legacy`, `generic` and `0` use the older generic layout. | `AgcExports.Init.cs` |
| `SHARPEMU_DISABLE_LLE_LIBC` | `1` | Stops the use of the guest libc module for libc exports. The emulator then uses its HLE handlers for these exports. | `DirectExecutionBackend.cs` |
| `SHARPEMU_HLE_FAST_MEMCPY` | `0` | Set to `0` to stop the native fast-path stub for the guest libc `memcpy` and `memmove`, so those imports use the managed HLE gateway again. | `DirectExecutionBackend.FastPath.cs` |
| `SHARPEMU_HLE_FAST_PATH` | `0` | Set to `0` to stop the native fast-path stubs for `pthread_self` and `pthread_getspecific`, so those imports use the managed HLE gateway again. The stubs are on by default on macOS x64 and are off on any run that traces imports or pthreads. | `DirectExecutionBackend.FastPath.cs` |
| `SHARPEMU_HOLD_FIRST_FLIP_MS` | number | Stops the thread that submits the flip for this number of milliseconds on one guest flip. The range is 0 to 60000 and the default is 0 (off). `SHARPEMU_HOLD_FLIP_NUMBER` selects the flip. | `VideoOutExports.cs` |
| `SHARPEMU_HOLD_FLIP_NUMBER` | number | Selects the guest flip that `SHARPEMU_HOLD_FIRST_FLIP_MS` holds. The minimum is 1 and the default is 1 (the first flip). | `VideoOutExports.cs` |

View File

@ -22,6 +22,7 @@ public sealed unsafe partial class DirectExecutionBackend
None,
PthreadSelf,
PthreadGetspecific,
MemoryCopy,
}
// scePthreadSelf / pthread_self.
@ -32,6 +33,16 @@ public sealed unsafe partial class DirectExecutionBackend
private const string PthreadGetspecificNid = "eoht7mQOCmo";
private const string PosixPthreadGetspecificNid = "0-KXaS70xy4";
// memcpy / memmove.
private const string MemcpyNid = "Q3VBxCXhUHs";
private const string MemmoveNid = "+P6FRGH4LfA";
// Guest memory is identity-mapped and every fault the copy can take (write tracking, lazy
// commit) is resolved from the fault address alone, so the copy may run as guest-side code.
// SHARPEMU_HLE_FAST_MEMCPY=0 keeps it on the managed export.
private static readonly bool GuestMemoryCopyStubEnabled =
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_MEMCPY"), "0", StringComparison.Ordinal);
private uint _fastPathBlockTlsIndex = uint.MaxValue;
private bool _guestFastPathEnabled;
private int _guestFastPathStubCount;
@ -88,6 +99,7 @@ public sealed unsafe partial class DirectExecutionBackend
{
PthreadSelfNid or PosixPthreadSelfNid => GuestFastPathStub.PthreadSelf,
PthreadGetspecificNid or PosixPthreadGetspecificNid => GuestFastPathStub.PthreadGetspecific,
MemcpyNid or MemmoveNid => GuestFastPathStub.MemoryCopy,
_ => GuestFastPathStub.None,
};
@ -105,7 +117,9 @@ public sealed unsafe partial class DirectExecutionBackend
}
var kind = ClassifyGuestFastPathStub(nid);
if (kind == GuestFastPathStub.None)
if (kind == GuestFastPathStub.None ||
// The write watch inspects every managed guest write, which a native copy never reaches.
(kind == GuestFastPathStub.MemoryCopy && (!GuestMemoryCopyStubEnabled || GuestWriteWatch.Armed)))
{
return false;
}
@ -154,6 +168,11 @@ public sealed unsafe partial class DirectExecutionBackend
uint blockTlsIndex,
nint fallbackTrampoline)
{
if (kind == GuestFastPathStub.MemoryCopy)
{
return EmitGuestMemoryCopyStub(fallbackTrampoline);
}
var code = new List<byte>(48);
var slowPathFixups = new List<int>();
@ -241,4 +260,27 @@ public sealed unsafe partial class DirectExecutionBackend
return code;
}
/// <summary>
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
/// export copies with memmove semantics, so a destination that starts inside the source
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
/// flag is clear on entry per the SysV ABI.
/// </summary>
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
{
var code = new List<byte>(40);
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
var slowPathFixup = code.Count - 1;
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
// jmp qword [rip+0] with the absolute trampoline address after it.
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
return code;
}
}

View File

@ -235,6 +235,103 @@ public sealed unsafe class GuestFastPathStubTests
return elapsed * 1_000_000_000.0 / Stopwatch.Frequency / iterations;
}
private const ulong MemoryCopySlowPathMarker = 0x5105_1A7B_AC4E_0001UL;
[Fact]
public void MemoryCopyStub_CopiesForwardAndReturnsTheDestination()
{
if (!CanRunStubs())
{
return;
}
using var stub = new MemoryCopyStub();
var source = Enumerable.Range(1, 300).Select(value => (byte)value).ToArray();
var destination = new byte[300];
fixed (byte* sourcePointer = source)
fixed (byte* destinationPointer = destination)
{
Assert.Equal((ulong)destinationPointer, stub.Call(destinationPointer, sourcePointer, 300));
Assert.Equal((ulong)destinationPointer, stub.Call(destinationPointer + 7, sourcePointer, 0) - 7);
}
Assert.Equal(source, destination);
}
[Fact]
public void MemoryCopyStub_HandlesADestinationBelowAnOverlappingSource()
{
if (!CanRunStubs())
{
return;
}
using var stub = new MemoryCopyStub();
var buffer = Enumerable.Range(0, 64).Select(value => (byte)value).ToArray();
fixed (byte* pointer = buffer)
{
Assert.Equal((ulong)pointer, stub.Call(pointer, pointer + 8, 40));
}
Assert.Equal(Enumerable.Range(8, 40).Select(value => (byte)value), buffer.Take(40));
Assert.Equal(Enumerable.Range(40, 24).Select(value => (byte)value), buffer.Skip(40));
}
[Fact]
public void MemoryCopyStub_LeavesADestinationInsideTheSourceToTheManagedExport()
{
if (!CanRunStubs())
{
return;
}
using var stub = new MemoryCopyStub();
var buffer = Enumerable.Range(0, 64).Select(value => (byte)value).ToArray();
var original = buffer.ToArray();
fixed (byte* pointer = buffer)
{
Assert.Equal(MemoryCopySlowPathMarker, stub.Call(pointer + 8, pointer, 40));
Assert.Equal(MemoryCopySlowPathMarker, stub.Call(pointer, pointer, 1));
}
Assert.Equal(original, buffer);
}
/// <summary>The memcpy stub called with the platform (SysV) convention the guest uses.</summary>
private sealed class MemoryCopyStub : IDisposable
{
private readonly byte* _page;
public MemoryCopyStub()
{
_page = (byte*)HostMemory.Alloc(
null,
4096,
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
HostMemory.PAGE_EXECUTE_READWRITE);
Assert.True(_page != null);
// Fallback at +0x100: mov rax, marker / ret.
_page[0x100] = 0x48;
_page[0x101] = 0xB8;
*(ulong*)(_page + 0x102) = MemoryCopySlowPathMarker;
_page[0x10A] = 0xC3;
var stub = DirectExecutionBackend.EmitGuestMemoryCopyStub((nint)(_page + 0x100));
for (var i = 0; i < stub.Count; i++)
{
_page[i] = stub[i];
}
Assert.True(HostMemory.Protect(_page, 4096, HostMemory.PAGE_EXECUTE_READ, out _));
}
public ulong Call(byte* destination, byte* source, nuint count) =>
((delegate* unmanaged<byte*, byte*, nuint, ulong>)_page)(destination, source, count);
public void Dispose() => _ = HostMemory.Free(_page, 0, HostMemory.MEM_RELEASE);
}
private static bool CanRunStubs() =>
OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.X64;