mmap()与读取块

我正在开发一个程序,用于处理可能达到100GB或更大容量的文件。 这些文件包含可变长度logging的集合。 我已经完成了第一个实现,现在正在寻求提高性能,特别是在更高效地执行I / O操作之后,因为input文件被多次扫描。

有没有经验法则使用mmap()与通过C ++的fstream库读取块? 我想要做的是从磁盘读取大块到缓冲区,从缓冲区处理完整的logging,然后阅读更多。

mmap()代码可能会变得非常混乱,因为mmap的d块需要位于页面大小的边界(我的理解),logging可能跨越页面边界。 有了fstream ,我只需要开始logging并重新阅读,因为我们不限于阅读位于页面大小边界上的块。

如何在不实际编写完整实现的情况下,在这两个选项之间做出决定? 任何经验法则(例如, mmap()快两倍)或简单的testing?

我试图在Linux上find关于mmap / read性能的最后一个字,我在Linux内核邮件列表中发现了一个很好的post( 链接 )。 从2000年开始,内核中的IO和虚拟内存已经有了很多改进,但是很好地解释了mmap或者read可能会更快或者更慢的原因。

  • mmap的调用比read有更多的开销(就像epollpoll有更多的开销,比开销epoll更多)。 在一些处理器上改变虚拟内存映射是相当昂贵的操作,其原因与在不同进程之间切换是昂贵的相同。
  • IO系统已经可以使用磁盘caching,所以如果你读取一个文件,无论你使用什么方法,你都会打开caching或错过它。

然而,

  • 内存映射通常对于随机访问更快,特别是如果您的访问模式是稀疏和不可预测的。
  • 内存映射允许您继续使用caching中的页面,直到完成。 这意味着如果您长时间使用某个文件,然后closures它并重新打开,页面仍然会被caching。 随着read ,你的文件可能已经从caching从年龄刷新。 如果您使用文件并立即丢弃它,这不适用。 (如果为了保持caching而尝试使用mlock页面,则试图超越磁盘caching,这种情况很less有助于系统性能)。
  • 直接读取文件非常简单快捷。

mmap / read的讨论让我想起了另外两个性能讨论:

  • 一些Java程序员惊讶地发现,非阻塞I / O通常比阻塞I / O要慢,如果知道非阻塞I / O需要更多的系统调用,这是非常有意义的。

  • 一些其他的networking程序员感到震惊,知道epoll通常比poll要慢,如果你知道pipe理epoll需要做更多的系统调用,这是非常有意义的。

结论:如果您随机访问数据,长时间保存,或者您知道可以与其他进程共享(如果没有实际的共享, MAP_SHARED不是很有趣),则使用内存映射。 如果您按顺序访问数据或在读取后将其丢弃,则可以正常读取文件。 如果任何一种方法都使得你的程序不那么复杂,就这样做。 对于许多现实世界的情况,没有一个确定的方法来显示一个更快,没有testing你的实际应用程序,而不是一个基准。

(对不起这个问题,但我正在寻找答案,这个问题不断出现在谷歌的结果顶部。)

mmap的速度更快。 你可以写一个简单的基准来certificate自己:

 char data[0x1000]; std::ifstream in("file.bin"); while (in) { in.read(data, 0x1000); // do something with data } 

与:

 const int file_size=something; const int page_size=0x1000; int off=0; void *data; int fd = open("filename.bin", O_RDONLY); while (off < file_size) { data = mmap(NULL, page_size, PROT_READ, 0, fd, off); // do stuff with data munmap(data, page_size); off += page_size; } 

显然,我没有详细说明(例如,如果在文件不是page_size的倍数的情况下,如何确定何时到达文件末尾),但实际上不应该比这个。

如果可以的话,你可以尝试把你的数据分解成多个文件,这些文件可以是整个mmap(),而不是部分(更简单)。

几个月前,我为boost_iostreams实现了一个滑动窗口mmap()的stream类的实现,但没有人关心,我忙于其他的东西。 最不幸的是,我几个星期前删除了一些旧的未完成项目的档案,这是其中一个受害者:-(

更新 :我还应该补充说明,这个基准testing在Windows中看起来完全不同,因为微软实现了一个漂亮的文件caching,它可以处理大部分的mmap操作。 也就是说,对于经常访问的文件,你可以只执行std :: ifstream.read(),它会和mmap一样快,因为文件caching已经为你做了内存映射,而且是透明的。

最后更新 :看看,人们:跨OS和标准库以及磁盘和内存层次结构的许多不同的平台组合,我不能肯定地说系统调用mmap ,被视为一个黑匣子,永远总是永远是实质性的快于read 。 这不完全是我的意图,即使我的话可以这样解释。 最后,我的观点是,内存映射I / O通常比基于字节的I / O更快; 这仍然是事实 。 如果你从实验上发现两者之间没有什么区别,那么对我来说唯一的解释就是,你的平台在封面下实现内存映射,这对于read调用的性能是有利的。 唯一可以确定的方式是以便携的方式使用内存映射的I / O,即使用mmap 。 如果您不关心可移植性,并且可以依赖目标平台的特定特性,那么在不牺牲性能的前提下使用read可能是合适的。

编辑清理答案清单: @jbl:

滑动窗口mmap听起来很有趣。 你能多说一点吗?

当然 – 我正在为Git编写一个C ++库(如果你愿意的话,可以使用一个libgit ++),并且遇到类似的问题:我需要能够打开大型(非常大)的文件, (因为它会与std::fstream )。

Boost::Iostreams已经有了一个mapped_file Source,但问题是它是mmap ping整个文件,这限制你2 ^(wordsize)。 在32位机器上,4GB不够大。 指望Git中的.pack文件变得比这个大得多,所以我不需要使用常规的文件I / O就可以大块读取文件,这并不是不合理的。 在Boost::Iostreams的封面下,我实现了一个Source,这个或多或less是std::streambufstd::istream之间交互的另一个视图。 你也可以通过inheritancestd::filebufmapped_filebuf来尝试类似的方法,同样的, std::fstreaminheritance到a mapped_fstream 。 这两者之间的相互作用是很难正确的。 Boost::Iostreams为您完成了一些工作,并且还提供了filter和链的钩子,所以我认为这样做会更有用。

主要的性能成本将是磁盘I / O。 “mmap()”肯定比istream快,但差异可能不明显,因为磁盘I / O将主宰你的运行时间。

我尝试了本·柯林斯的代码片段(参见上面/下面)来testing他的断言:“mmap()速度更快”并且没有发现可测量的差异。 看到我的评论他的答案。

我肯定不会build议分别mmap每个logging轮stream,除非你的“logging”是巨大的 – 这将是非常缓慢,每个logging需要2个系统调用,并可能失去了磁盘内存caching的页面…. 。

在你的情况下,我认为mmap(),istream和低级别的open()/ read()调用将大致相同。 我会在这些情况下推荐mmap():

  1. 在文件中有随机存取(不是连续的),AND
  2. 整个事情可以舒适地放在内存中,或者在文件中有引用的局部性,以便某些页面可以被映射,而其他页面被映射出来。 这样操作系统使用可用的RAM来获得最大的收益。
  3. 或者如果多个进程正在读取/处理同一个文件,那么mmap()就非常棒了,因为进程都共享相同的物理页面。

(顺便说一句 – 我爱mmap()/ MapViewOfFile())。

这里已经有很多很好的答案了,所以我只会添加一些我没有看到的问题。

mmap看起来很神奇

以文件已经完全caching1的情况作为基线2mmap可能看起来非常像魔术

  1. mmap只需要1次系统调用(可能)映射整个文件,之后不再需要系统调用。
  2. mmap不需要从内核到用户空间的文件数据副本。
  3. mmap允许你访问“作为内存”的文件,包括使用你可以对内存做的任何高级技巧来处理它,比如编译器自动向量化, SIMD内在函数,预取,优化的内存分析例程,OpenMP等等。

在文件已经在caching中的情况下,似乎不可能打败你:只是直接访问内核页面caching作为内存,并且不能比这更快。

那么可以。

mmap实际上并不神奇,因为…

mmap做每页工作

mmap vs read(2)的主要隐藏成本(这实际上是用于读取块的可比操作系统级系统调用)是,使用mmap您将需要为用户空间中的每个4K页面执行“一些工作”,即使它可能被页面错误机制隐藏起来。

举个例子,一个典型的实现只需要mmap整个文件,就需要100GB / 4K = 2500万个故障来读取一个100GB的文件。 现在,这些将是小错 ,但是250亿页错误仍然不会超速。 在最好的情况下,一个小故障的代价可能在100纳米左右。

mmap严重依赖于TLB的性能

现在,您可以将MAP_POPULATE传递给mmap来告诉它在返回之前设置所有的页表,所以在访问时不应该出现页面错误。 现在,这个小问题也会把整个文件读到RAM中,如果你试图映射一个100GB的文件,这个文件将会被炸毁 – 但是现在让我们忽略这个问题3 。 内核需要做每页工作来设置这些页表(显示为内核时间)。 这最终成为mmap方法的一个主要成本,并且与文件大小成正比(即,随着文件大小的增长,它不会变得相对不重要) 4

最后,即使在用户空间访问中,这样的映射也不是完全免费的(与不是源自基于文件的mmap大内存缓冲区相比),即使设置了页表,每次访问新页面都将在概念上,招致TLB的怀念。 由于mmap文件意味着使用页面caching及其4K页面,因此再次为100GB文件花费2500万次。

现在,这些TLB缺失的实际成本至less在很大程度上取决于硬件的以下几个方面:(a)你拥有多less个4K TLB,以及其他翻译caching工作如何执行(b)硬件预取如何处理与TLB – 例如,可以预取触发页面散步? (c)页面行走硬件的速度和平行度如何。 在现代高端x86 Intel处理器上,页面散步硬件通常非常强大:至less有两个并行页面散步器,页面散步可以与继续执行同时发生,硬件预取可以触发页面散步。 所以TLB 对stream式读取负载的影响是相当低的 – 而且这样的负载通常会类似地执行,不pipe页面大小如何。 但是其他的硬件通常更糟糕!

read()避免了这些陷阱

read()系统调用是C,C ++和其他语言中提供的“块读取”types调用的基本缺点,它有一个主要缺点,即每个人都清楚:

  • 每个N字节的read()调用都必须从内核拷贝N个字节到用户空间。

另一方面,它避免了上述的大部分成本 – 您不需要将2500万个4K页面映射到使用空间。 您通常可以在用户空间中使用单个缓冲区小缓冲区,并重复使用您的所有read调用。 在内核方面,几乎没有4K页面或TLB未命中的问题,因为所有的RAM通常都是使用几个非常大的页面(例如,x86上的1 GB页面)进行线性映射的,因此页面caching中的底层页面被覆盖在内核空间非常有效。

所以基本上你有以下比较来确定单个大文件读取哪个更快:

mmap方法所隐含的额外每页工作是否比使用read()所隐含的将文件内容从内核复制到用户空间的每字节工作成本更高?

在许多系统中,它们实际上是大致平衡的。 请注意,每个硬件和OS堆栈的属性完全不同。

尤其是,在以下情况下, mmap方法变得相对更快:

  • 操作系统具有快速的轻微故障处理,特别是小故障膨胀优化,如故障转移。
  • 操作系统有一个很好的MAP_POPULATE实现,在底层页面在物理内存中连续的情况下,可以高效地处理大型地图。
  • 硬件具有较强的页面翻译性能,如大的TLB,快速的二级TLB,快速并行的页面转换,预取与翻译的良好交互等。

…而read()方法变得相对更快的时候:

  • read()系统调用具有良好的复制性能。 例如,在内核方面有很好的copy_to_user性能。
  • 内核有一个有效的(相对于用户空间)映射内存的方法,例如,只使用几个硬件支持的大页面。
  • 内核具有快速的系统调用,并且可以跨系统调用保持内核TLB条目。

上述硬件因素在不同的平台上,即使是在同一个系列(例如,在x86世代,特别是在市场细分市场中)以及绝对跨体系结构(例如,ARM vs x86与PPC)内也是千差万别的。

操作系统因素也在不断变化,双方的各种改进导致一种方法或另一种方法的相对速度大幅度提高。 最近的名单包括:

  • 如上所述,除了MAP_POPULATE之外,还可以增加mmap情况。
  • arch/x86/lib/copy_user_64.S添加了快速pathcopy_to_user方法,例如,在快速时使用REP MOVQ ,这对read()情况确实有帮助。

1这或多或less包括文件没有被完全caching的情况,但是在预读操作系统的情况下,它足以让它看起来如此(例如,页面通常被caching到你的时间想要它)。 这是一个微妙的问题,因为预读工作的方式在mmapread调用之间往往是完全不同的,并且可以通过2中描述的“advise”调用进一步调整。

2 …因为如果文件没有被caching,你的行为将完全由IO的关注所控制,包括你的访问模式对底层硬件是多么的同情 – 所有的努力应该是确保这样的访问同样可以例如通过使用madvisefadvise调用(以及可以改进访问模式的任何应用程序级别更改)。

3例如,可以通过在较小的窗口(例如100 MB)中顺序地进行mmap来解决这个问题。

4事实上, MAP_POPULATE方法(至less有一个硬件/操作系统的组合)比不使用它稍微快一点,可能是因为内核使用了故障 – 所以实际的小故障数量减less了一个因子16左右。

对不起本科林斯失去了滑动窗口的mmap源代码。 这在Boost中会很好。

是的,映射文件要快得多。 您基本上使用操作系统虚拟内存子系统关联内存到磁盘,反之亦然。 这样想一想:如果操作系统内核开发人员能够加快速度,他们会的。 因为这样做可以更快地完成所有任务:数据库,启动时间,程序加载时间等等。

滑动窗口方法确实不是那么困难,因为可以同时映射多个continguous页面。 所以只要单个logging中最大的logging可以logging,logging的大小就不重要了。 重要的是pipe理簿记。

如果logging没有在getpagesize()边界上开始,则映射必须从前一页开始。 映射区域的长度从logging的第一个字节开始(如有必要,向下舍入为getpagesize()的最接近倍数)到logging的最后一个字节(四舍五入为getpagesize()的最接近倍数)。 当你完成一个logging的处理时,你可以取消映射(),然后转到下一个logging。

这一切都工作得很好,在Windows下使用CreateFileMapping()和MapViewOfFile()(和GetSystemInfo()获取SYSTEM_INFO.dwAllocationGranularity —而不是SYSTEM_INFO.dwPageSize)。

mmap应该更快,但我不知道多less。 它非常依赖于你的代码。 如果你使用mmap,最好一次完成整个文件的映射,这会让你的生活变得更容易。 一个潜在的问题是,如果你的文件大于4GB(或者实际上限制较低,通常是2GB),你将需要64位体系结构。 所以如果你使用32位的环境,你可能不想使用它。

话虽如此,改善绩效可能有更好的途径。 你说input文件被扫描了很多次 ,如果你能一遍读出来,然后用它来完成,可能会更快。

我同意mmap'd文件I / O将会更快,但是当您对代码进行基准testing时,是否不应该对计数器示例进行某种优化?

本·柯林斯写道:

 char data[0x1000]; std::ifstream in("file.bin"); while (in) { in.read(data, 0x1000); // do something with data } 

我build议也尝试:

 char data[0x1000]; std::ifstream iifle( "file.bin"); std::istream in( ifile.rdbuf() ); while( in ) { in.read( data, 0x1000); // do something with data } 

除此之外,您也可以尝试使缓冲区大小与虚拟内存的一页相同,以防0x1000不是虚拟内存页面的大小…恕我直言,mmap文件I / O仍然胜利,但这应该使事情更接近。

也许你应该预处理文件,所以每个logging都在一个单独的文件(或至less每个文件是一个mmap大小)。

在进入下一个logging之前,你也可以为每个logging做所有的处理步骤吗? 也许这会避免一些IO开销?

我记得好几年前,一个包含树结构的巨大文件映射到内存中。 与正常的反序列化相比,我感到惊讶,这个反序列化涉及很多内存中的工作,比如分配树节点和设置指针。 所以实际上,我只是将一个调用与mmap(或Windows上的对应部分)进行比较,然后对运算符new和构造函数调用的许多(MANY)调用进行比较。 对于这样的任务,与反序列化相比,mmap是无与伦比的。 当然,我们应该看看这个增强可重定位指针。

这听起来像是一个很好的multithreading使用情况…我想你可以很容易地设置一个线程来读取数据,而其他(S)处理它。 这可能是一种显着提高感知性能的方法。 只是一个想法。

在我看来,使用mmap()“只是”使开发人员免于编写自己的caching代码。 在一个简单的“通过一次读取文件”的情况下,这不会很难(尽pipemlbrock指出你仍然将内存拷贝保存到进程空间中),但是如果你在文件中来回移动,或者跳过比特等等,我相信内核开发者可能做得比实现更好的caching比我可以…

我认为关于mmap的最重要的事情是用asynchronous阅读的潜力:

  addr1 = NULL; while( size_left > 0 ) { r = min(MMAP_SIZE, size_left); addr2 = mmap(NULL, r, PROT_READ, MAP_FLAGS, 0, pos); if (addr1 != NULL) { /* process mmap from prev cycle */ feed_data(ctx, addr1, MMAP_SIZE); munmap(addr1, MMAP_SIZE); } addr1 = addr2; size_left -= r; pos += r; } feed_data(ctx, addr1, r); munmap(addr1, r); 

问题是,我无法find正确的MAP_FLAGS来提示这个内存应该尽快从文件同步。 我希望MAP_POPULATE为mmap提供了正确的提示(也就是说,它不会尝试在从调用返回之前加载所有内容,但是会使用feed_data以asynchronous的方式执行此操作)。 至less这个标志给出了更好的结果,即使那个手册也说明了自2.6.23以来,没有MAP_PRIVATE,它就什么都不做。