-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
Copying bytes to a memory-mapped file without using unsafe is currently about 30 times slower than it should be.
This is because the current API for UnmanagedMemoryAccessor only provides a WriteArray<T> method that is templated to value types, and internally it uses a struct marshaller in a for loop to copy each value of the input array to the output file. When the struct type is byte, the marshaler does nothing except destroy all compiler fast-copy optimizations and make the process needlessly slow.
A developer today could work around this by using unsafe code and byte* pointers via SafeMemoryMappedViewHandle.AcquirePointer, but obviously this puts the codebase into an unsafe context and opens the possibility of buffer overrun bugs / exploits via IPC.
Improving the read/write IO performance of memory-mapped files will allow new code to utilize the performance and safety benefits of Span for any IPC scenarios (databases connections, containers, etc.). All current users of UnmanagedMemoryAccessor will remain untouched until they opt-in to this new function.
Quick benchmarks showing the current state of MMIO copy performance:
| Method | ByteCount | Mean | Error | StdDev | Ratio | RatioSD |
|---|---|---|---|---|---|---|
| AcquirePointerWrite | 1024 | 19.96 ns | 0.117 ns | 0.103 ns | 1.00 | 0.01 |
| WriteArray | 1024 | 584.42 ns | 1.796 ns | 1.680 ns | 29.29 | 0.17 |
| AcquirePointerWrite | 65536 | 999.49 ns | 4.332 ns | 3.617 ns | 1.00 | 0.00 |
| WriteArray | 65536 | 36,558.33 ns | 86.388 ns | 80.808 ns | 36.58 | 0.15 |
API Proposal
namespace System.IO
public class UnmanagedMemoryAccessor
{
public int Read(long position, Span<byte> buffer) { }
public void Write(long position, ReadOnlySpan<byte> buffer) { }
}Most of this work has already been done when similar Read/Write methods were added to SafeBuffer years ago. These new functions would basically be shims from UnmanagedMemoryAccessor to matching calls on its inner SafeBuffer. The actual implementation would look like this.
API Usage
Where current code seeking high-performance would have to use something like this:
public void WriteToMMFile(MemoryMappedViewAccessor view, ReadOnlySpan<byte> dataToWrite, long offset)
{
unsafe
{
byte* ptr = null;
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
dataToWrite.CopyTo(new Span<byte>(ptr, dataToWrite.Length));
view.SafeMemoryMappedViewHandle.ReleasePointer();
}
}Could now be changed to simply this and remove the need for an unsafe context.
public void WriteToMMFile(MemoryMappedViewAccessor view, ReadOnlySpan<byte> dataToWrite, long offset)
{
view.Write(offset, dataToWrite)
}Likewise, any copy currently using WriteArray could be easily changed to use this new span implementation instead, getting the performance gain with almost no change to functionality.
Alternative Designs
No response
Risks
If we were only adding new Span<byte> read/write methods, there would be no change to any current code.
Potentially, we could add an overload for WriteArray<byte>(...) that triggers the span fast-path under the hood, which would affect existing code, but I doubt it would be a breaking change or performance regression.
Any change to UnmanagedMemoryAccessor would apply not only to MemoryMappedViewAccessor but also all of its other subclasses (What are those? How would they be affected?)