dotnet / dotnet/runtime

[API Proposal]: Expose functions for actually performant MemoryMappedFile byte copies

Open
#122,815 4 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.IO needs-further-triage tenet-performance
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.