Skip to content

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

@lostromb

Description

@lostromb

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?)

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.IOuntriagedNew issue has not been triaged by the area owner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions