如何在.NET中快速从内存映射文件中读取字节?

22
在某些情况下,MemoryMappedViewAccessor 类并不能高效地读取字节;我们最好得到的是通用的 ReadArray<byte>,对于所有结构都是相同的路线,当你只需要字节时就涉及多个不必要的步骤。

虽然可以使用 MemoryMappedViewStream,但由于它基于一个 Stream,因此您需要首先定位到正确的位置,然后读取操作本身有许多不必要的步骤。

在.NET中从内存映射文件快速高效地读取字节数组的方法是什么,考虑到应该只有特定的地址空间需要读取?

5个回答

34

这个解决方案需要使用不安全代码(使用/unsafe开关进行编译),但直接获取内存指针,然后可以使用Marshal.Copy。这比.NET框架提供的方法快得多。

    // assumes part of a class where _view is a MemoryMappedViewAccessor object

    public unsafe byte[] ReadBytes(int offset, int num)
    {
        byte[] arr = new byte[num];
        byte *ptr = (byte*)0;
        this._view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
        Marshal.Copy(IntPtr.Add(new IntPtr(ptr), offset), arr, 0, num);
        this._view.SafeMemoryMappedViewHandle.ReleasePointer();
        return arr;
    }

    public unsafe void WriteBytes(int offset, byte[] data)
    {
        byte* ptr = (byte*)0;
        this._view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
        Marshal.Copy(data, 0, IntPtr.Add(new IntPtr(ptr), offset), data.Length);
        this._view.SafeMemoryMappedViewHandle.ReleasePointer();
    }

6
为了确保在 Marshal.Copy 抛出异常时 ReleasePointer 也能够运行,你应该使用一个关键的执行块和一个 try-finally 块。 - Matt Howells
3
不错的答案 =) 实际上,分析显示使用托管封装器访问映射内存比使用不安全指针慢30倍。 - user1222021
2
@MattHowells 我同意。我读过CER可能会影响性能,但至少在一个受控测试中似乎可以忽略不计。无论性能影响如何,在这里的“备注”下描述的是正确的使用模式;https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.safebuffer.acquirepointer(v=vs.110).aspx - LaFleur

2

我知道这是一个旧问题,已经有答案了,但我想加上我的两分钱。

我对使用不安全代码的已接受答案和使用MemoryMappedViewStream方法读取200MB字节数组进行了测试。

MemoryMappedViewStream

        const int MMF_MAX_SIZE = 209_715_200;
        var buffer = new byte[ MMF_VIEW_SIZE ];

        using( var mmf = MemoryMappedFile.OpenExisting( "mmf1" ) )
        using( var view = mmf.CreateViewStream( 0, buffer.Length, MemoryMappedFileAccess.ReadWrite ) )  
        {
            if( view.CanRead )
            {
                Console.WriteLine( "Begin read" );
                sw.Start( );
                view.Read( buffer, 0, MMF_MAX_SIZE );
                sw.Stop( );
                Console.WriteLine( $"Read done - {sw.ElapsedMilliseconds}ms" );
            }
        }

我使用两种方法分别测试了三次,并记录下了以下时间。
MemoryMappedViewStream:
  1. 483毫秒
  2. 501毫秒
  3. 490毫秒
不安全的方法:
  1. 531毫秒
  2. 517毫秒
  3. 523毫秒
从这些少量测试结果看来,MemoryMappedViewStream略微占优势。因此,我建议日后阅读此帖子的任何人都采用MemoryMappedViewStream方法。请注意,保留html标签。

这很有道理,文档说要使用视图流进行顺序访问,它针对此进行了优化,而视图访问器则用于随机访问。 - mhand

2
请查看这个错误报告:No way to determine internal offset used by MemoryMappedViewAccessor - Makes SafeMemoryMappedViewHandle property unusable. 来自该报告的内容:
MemoryMappedViewAccessor有一个SafeMemoryMappedViewHandle属性,返回内部使用的ViewHandle,但没有任何属性返回MemoryMappedView使用的偏移量。
由于MemoryMappedView在MemoryMappedFile.CreateViewAccessor(offset,size)中请求的offset是页对齐的,因此如果不知道偏移量,就无法使用SafeMemoryMappedViewHandle做任何有用的工作。请注意,我们实际想要做的是使用AcquirePointer(ref byte* pointer)方法允许一些快速的基于指针(可能是非托管的)代码运行。我们可以接受指针页对齐,但必须能够找出与最初请求的地址相比的偏移量。

这似乎很傻..如果您控制视图,则不需要.NET告诉您偏移量,因为您已经指定了它。(这就是我所做的:_view是偏移量为0的访问器) - Kieren Johnstone
顺便说一下,这段代码也经过了多次压力测试 [数十亿次调用,成千上万个不同的 MMF] 在几台机器上。 - Kieren Johnstone
现在有数百亿个调用和成千上万个MMF。这个bug在我的代码中不会发生 ;) - Kieren Johnstone
3
现在似乎已经修复了 - 您可以访问MemoryMappedViewAccessor的PointerOffset属性,以计算与请求的偏移量相对应的正确指针地址。只需将PointerOffset添加到由SafeMemoryMappedViewHandle.AcquirePointer()返回的地址即可。 - RobinG

1
这个解决方案的安全版本是:
var file = MemoryMappedFile.CreateFromFile(...);
var accessor = file.CreateViewAccessor();
var bytes = new byte[yourLength];

// assuming the string is at the start of the file
// aka position: 0
// https://msdn.microsoft.com/en-us/library/dd267761(v=vs.110).aspx
accessor.ReadArray<byte>(
    position: 0,      // The number of bytes in the accessor at which to begin reading
    array: bytes,     // The array to contain the structures read from the accessor
    offset: 0,        // The index in `array` in which to place the first copied structure
    count: yourLength // The number of structures of type T to read from the accessor.
);

var myString = Encoding.UTF8.GetString(bytes);

我已经测试过了,它是有效的。我无法评论它的性能或是否是最佳的整体解决方案,只能说它有效。

1
很酷,肯定避免使用指针 :) 但是“ReadArray<byte>”会慢得多。 - Kieren Johnstone

0

只是想分享一个版本,它具有的l_offset(因此可以读取\写入大于int32最大大小的文件):

public static unsafe byte[] ReadBytes(long l_offset, int i_read_buf_size, MemoryMappedViewAccessor mmva)
    {
        byte[] arr = new byte[i_read_buf_size];
        byte* ptr = (byte*)0;
        mmva.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
        IntPtr p = new(ptr);
        Marshal.Copy(new IntPtr(p.ToInt64() + l_offset), arr, 0, i_read_buf_size);
        mmva.SafeMemoryMappedViewHandle.ReleasePointer();
        return arr;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接