[API Proposal]: Expose functions for actually performant MemoryMappedFile byte copies
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
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` 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
```csharp
namespace System.IO
public class UnmanagedMemoryAccessor
{
public int Read(long position, Span buffer) { }
public void Write(long position, ReadOnlySpan 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](https://github.com/dotnet/runtime/pull/118296/files).
### API Usage
Where current code seeking high-performance would have to use something like this:
```csharp
public void WriteToMMFile(MemoryMappedViewAccessor view, ReadOnlySpan dataToWrite, long offset)
{
unsafe
{
byte* ptr = null;
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
dataToWrite.CopyTo(new Span(ptr, dataToWrite.Length));
view.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
```
Could now be changed to simply this and remove the need for an unsafe context.
```csharp
public void WriteToMMFile(MemoryMappedViewAccessor view, ReadOnlySpan 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` read/write methods, there would be no change to any current code.
Potentially, we could add an overload for `WriteArray(...)` 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?)
Contributor guide
Assessment
This issue has not been assessed yet.