写入内存映射文件比非内存映射文件慢

时间:2015-11-21 11:29:36

标签: c# performance io memory-mapped-files

我正在尝试使用内存映射文件来编写具有高IO要求的应用程序。在这个应用程序中,我收到的数据突发速度比磁盘能够支持的速度快。为了避免在我的应用程序中缓冲逻辑,我考虑使用内存映射文件。有了这种文件,我只会写入映射到文件的内存(比磁盘可以支持的速度快),操作系统最终将这些数据刷新到磁盘。因此操作系统正在为我做缓冲。

经过实验,我发现内存映射文件使得在内存中写入速度更快,但刷新到磁盘比使用普通文件慢。这就是我得出这个结论的原因。这是一段代码,它尽可能快地写入非内存映射文件:

    private static void WriteNonMemoryMappedFile(long fileSize, byte[] bufferToWrite)
    {
        Console.WriteLine(" ==> Non memory mapped file");

        string normalFileName = Path.Combine(Path.GetTempPath(), "MemoryMappedFileWriteTest-NonMmf.bin");
        if (File.Exists(normalFileName))
        {
            File.Delete(normalFileName);
        }

        var stopWatch = Stopwatch.StartNew();
        using (var file = File.OpenWrite(normalFileName))
        {
            var numberOfPages = fileSize/bufferToWrite.Length;

            for (int page = 0; page < numberOfPages; page++)
            {
                file.Write(bufferToWrite, 0, bufferToWrite.Length);
            }
        }

        Console.WriteLine("Non-memory mapped file is now closed after {0} seconds ({1} MB/s)", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
    }

此代码导致:

==> Non memory mapped file
Non-memory mapped file is now closed after 10.5918587 seconds (966.687541390441 MB/s)

如您所见,我的磁盘非常快。这将是我对内存映射文件的基准。

现在我尝试使用不安全的代码将相同的数据写入内存映射文件(因为这是我打算在我的应用程序中执行的操作):

    [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
    public static extern IntPtr memcpy(IntPtr dest, IntPtr src, UIntPtr count);

    private static unsafe void WriteMemoryMappedFileWithUnsafeCode(long fileSize, byte[] bufferToWrite)
    {
        Console.WriteLine(" ==> Memory mapped file with unsafe code");

        string fileName = Path.Combine(Path.GetTempPath(), "MemoryMappedFileWriteTest-MmfUnsafeCode.bin");
        if (File.Exists(fileName))
        {
            File.Delete(fileName);
        }

        string mapName = Guid.NewGuid().ToString();

        var stopWatch = Stopwatch.StartNew();
        using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(fileName, FileMode.Create, mapName, fileSize, MemoryMappedFileAccess.ReadWrite))
        using (var view = memoryMappedFile.CreateViewAccessor(0, fileSize, MemoryMappedFileAccess.Write))
        {
            unsafe
            {
                fixed (byte* pageToWritePointer = bufferToWrite)
                {
                    byte* pointer = null;
                    try
                    {
                        view.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);

                        var writePointer = pointer;

                        var numberOfPages = fileSize/bufferToWrite.Length;

                        for (int page = 0; page < numberOfPages; page++)
                        {
                            memcpy((IntPtr) writePointer, (IntPtr) pageToWritePointer, (UIntPtr) bufferToWrite.Length);
                            writePointer += bufferToWrite.Length;
                        }
                    }
                    finally
                    {
                        if (pointer != null)
                            view.SafeMemoryMappedViewHandle.ReleasePointer();
                    }
                }
            }

            Console.WriteLine("All bytes written in MMF after {0} seconds ({1} MB/s). Will now close MMF. This may be long since some content may not have been flushed to disk yet.", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
        }

        Console.WriteLine("File is now closed after {0} seconds ({1} MB/s)", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
    }

然后我明白了:

==> Memory mapped file with unsafe code
All bytes written in MMF after 6.5442406 seconds (1564.73302033172 MB/s). Will now close MMF. This may be long since some content may not have been flushed to disk yet.
File is now closed after 18.8873186 seconds (542.162704287661 MB/s)

正如您所看到的,这要慢得多。它写入大约56%的非内存映射文件。

然后我尝试了另一件事。我试图使用ViewStreamAccessor而不是不安全的代码:

    private static unsafe void WriteMemoryMappedFileWithViewStream(long fileSize, byte[] bufferToWrite)
    {
        Console.WriteLine(" ==> Memory mapped file with view stream");
        string fileName = Path.Combine(Path.GetTempPath(), "MemoryMappedFileWriteTest-MmfViewStream.bin");
        if (File.Exists(fileName))
        {
            File.Delete(fileName);
        }

        string mapName = Guid.NewGuid().ToString();

        var stopWatch = Stopwatch.StartNew();
        using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(fileName, FileMode.Create, mapName, fileSize, MemoryMappedFileAccess.ReadWrite))
        using (var viewStream = memoryMappedFile.CreateViewStream(0, fileSize, MemoryMappedFileAccess.Write))
        {
            var numberOfPages = fileSize / bufferToWrite.Length;

            for (int page = 0; page < numberOfPages; page++)
            {
                viewStream.Write(bufferToWrite, 0, bufferToWrite.Length);
            }                

            Console.WriteLine("All bytes written in MMF after {0} seconds ({1} MB/s). Will now close MMF. This may be long since some content may not have been flushed to disk yet.", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
        }

        Console.WriteLine("File is now closed after {0} seconds ({1} MB/s)", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
    }

然后我明白了:

==> Memory mapped file with view stream
All bytes written in MMF after 4.6713875 seconds (2192.06548076352 MB/s). Will now close MMF. This may be long since some content may not have been flushed to disk yet.
File is now closed after 16.8921666 seconds (606.198141569359 MB/s)

再一次,这比使用非内存映射文件慢得多。

那么,有没有人知道如何在写入时使内存映射文件与非内存映射文件一样快?

顺便说一下,这是我测试程序的其余部分:

    static void Main(string[] args)
    {
        var bufferToWrite = Enumerable.Range(0, Environment.SystemPageSize * 256).Select(i => (byte)i).ToArray();
        long fileSize = 10 * 1024 * 1024 * 1024L; // 2 GB

        WriteNonMemoryMappedFile(fileSize, bufferToWrite);
        WriteMemoryMappedFileWithUnsafeCode(fileSize, bufferToWrite);
        WriteMemoryMappedFileWithViewStream(fileSize, bufferToWrite);
    }

    private static double GetSpeed(long fileSize, Stopwatch stopwatch)
    {
        var mb = fileSize / 1024.0 / 1024.0;
        var mbPerSec = mb / stopwatch.Elapsed.TotalSeconds;
        return mbPerSec;
    }

编辑1:

正如usr所建议的,我尝试使用SequenctialScan选项。不幸的是,它没有任何影响。这是我所做的改变:

        using (var file = new FileStream(fileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.SequentialScan))
        using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(file, mapName, fileSize, MemoryMappedFileAccess.ReadWrite, null, HandleInheritability.None, leaveOpen: false))

2 个答案:

答案 0 :(得分:3)

来自SDK documentation

  

未映射视图中的已修改页面在共享计数达到零之前不会写入磁盘,换句话说,直到它们从共享页面的所有进程的工作集中取消映射或修剪。即使这样,修改后的页面也会“懒洋洋地”写入磁盘;也就是说,修改可以缓存在内存中并在以后写入磁盘。为了在发生电源故障或系统崩溃时将数据丢失的风险降至最低,应用程序应使用FlushViewOfFile函数显式刷新已修改的页面。

.NET程序员认真对待最后一句话,你调用的MemoryMappedViewStream.Dispose() method确实调用了FlushViewOfFile()。这需要时间,你会在你的个人资料结果中看到这一点。技术上可以绕过这个调用,不要调用Dispose()并让终结器关闭视图句柄。

FileStream不会对文件(FlushFileBuffers)执行等效操作,因此您可以充分利用从文件系统缓存到磁盘的延迟写入。在Dispose()调用之后很久就会发生,无法观察到你的程序。

答案 1 :(得分:0)

我遇到了类似的性能问题。作为对Hans答案的扩展,我相信以下方法将允许OS通过布置SafeMemoryMappedViewHandle而不是Stream来异步将修改后的数据刷新到文件系统。

private static unsafe void WriteMemoryMappedFileWithViewStream(long fileSize, byte[] bufferToWrite)
{
    Console.WriteLine(" ==> Memory mapped file with view stream");
    string fileName = Path.Combine(Path.GetTempPath(), "MemoryMappedFileWriteTest-MmfViewStream.bin");
    if (File.Exists(fileName))
    {
        File.Delete(fileName);
    }

    string mapName = Guid.NewGuid().ToString();

    var stopWatch = Stopwatch.StartNew();
    using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(fileName, FileMode.Create, mapName, fileSize, MemoryMappedFileAccess.ReadWrite))
    {
        var viewStream = memoryMappedFile.CreateViewStream(0, fileSize, MemoryMappedFileAccess.Write);
        using (viewStream.SafeMemoryMappedViewHandle)
        {
            var numberOfPages = fileSize / bufferToWrite.Length;

            for (int page = 0; page < numberOfPages; page++)
            {
                viewStream.Write(bufferToWrite, 0, bufferToWrite.Length);
            }                

            Console.WriteLine("All bytes written in MMF after {0} seconds ({1} MB/s). Will now close MMF. This may be long since some content may not have been flushed to disk yet.", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
        }
    }

    Console.WriteLine("File is now closed after {0} seconds ({1} MB/s)", stopWatch.Elapsed.TotalSeconds, GetSpeed(fileSize, stopWatch));
}