写入内存映射文件比写入非内存映射文件慢
本文关键字:文件 映射 内存 | 更新日期: 2023-09-27 18:23:48
我正在尝试使用内存映射文件来编写具有高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的建议,我尝试使用SequentialScan选项。不幸的是,它没有产生任何影响。这是我所做的改变:
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))
来自SDK文档:
在未映射视图中修改的页面直到它们的共享计数达到零,或者换句话说,直到它们从共享页面的所有进程的工作集中被取消映射或修剪,才会写入磁盘。即便如此,修改后的页面也会"懒散地"写入磁盘;也就是说,修改可以缓存在内存中,并在以后写入磁盘。为了最大限度地降低电源故障或系统崩溃时数据丢失的风险,应用程序应使用FlushViewOfFile函数显式刷新修改后的页面。
.NET程序员认真对待最后一句话,您正在调用的MemoryMappedViewStream.Dispose()方法实际上调用了FlushViewOfFile()。这需要时间,你会在你的个人资料结果中看到这一点。从技术上讲,可以绕过此调用,不要调用Dispose(),而是让终结器关闭视图句柄。
FileStream对文件(FlushFileBuffers)没有做等效的操作,因此您可以从文件系统缓存到磁盘的延迟写入中获得全部好处。发生在Dispose()调用之后很长一段时间,对您的程序来说是不可观察的。
我遇到了类似的性能问题。作为对Hans答案的扩展,我相信以下方法将允许操作系统通过处理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));
}