mmap()与阅读块

jbl*_*jbl 172 c++ file-io fstream mmap

我正在开发一个程序,该程序将处理可能大小为100GB或更大的文件.这些文件包含一组可变长度记录.我已经启动并运行了第一个实现,现在我正在寻求提高性能,特别是在输入文件被多次扫描时更有效地进行I/O.

mmap()通过C++的fstream库使用和读取块有经验吗?我想做的是从磁盘读取大块到缓冲区,从缓冲区处理完整记录,然后阅读更多.

mmap()代码可能会变得非常凌乱,因为mmap"d块需要躺在页大小的边界(我的理解)和记录可能潜在般划过页面边界.使用fstreams,我可以寻找记录的开头并再次开始阅读,因为我们不仅限于阅读位于页面大小边界的块.

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

Die*_*Epp 193

我试图在Linux上找到关于mmap/read性能的最后一句话,我在Linux内核邮件列表上遇到了一个很好的帖子(链接).它是从2000年开始的,因此从那时起内核中的IO和虚拟内存已经有了很多改进,但它很好地解释了为什么mmap或者read可能更快或更慢的原因.

  • 调用mmap具有更多的开销read(就像epoll有更多的开销,比poll有更多的开销read).在某些处理器上更改虚拟内存映射是一项非常昂贵的操作,原因与不同进程之间的切换成本高昂相同.
  • IO系统已经可以使用磁盘高速缓存,所以如果你读取一个文件,你会打的缓存或错过它,不管你用什么方法.

然而,

  • 对于随机访问,内存映射通常更快,特别是如果您的访问模式稀疏且不可预测.
  • 内存映射允许您继续使用缓存中的页面,直到完成为止.这意味着如果您长时间大量使用文件,然后关闭它并重新打开它,页面仍将被缓存.有了read,您的文件可能早已从缓存中刷新.如果您使用文件并立即丢弃它,则不适用.(如果您尝试将mlock页面保留在缓存中,那么您正试图超越磁盘缓存,这种类型的漏洞很少有助于系统性能).
  • 直接读取文件非常简单快捷.

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

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

  • 其他一些网络程序员感到震惊的epoll是,通常比较慢poll,如果你知道管理epoll需要进行更多的系统调用,那就非常有意义了.

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

(对不起,necro'ing这个问题,但我一直在寻找答案,这个问题一直想出在谷歌搜索结果的顶部.)

  • 嘿,"necro'ing"没问题.我说你肯定改进了这里的内容,这很受欢迎. (16认同)
  • 非常感谢你.我自己从Google登陆后,我不得不同意这应该是接受的答案. (14认同)
  • 请记住,使用任何基于 2000 年代硬件和软件的建议而不进行今天的测试将是一种非常可疑的方法。此外,虽然该线程中有关“mmap”与“read()”的许多事实仍然像过去一样正确,但整体性能并不能通过将优缺点相加来真正确定,而只能通过在特定的硬件配置上进行测试。例如,“对 mmap 的调用比 read 的开销更大”这一说法是有争议的——是的,“mmap”必须将映射添加到进程页表,但“read”必须将所有读取的字节从内核复制到用户空间。 (3认同)
  • @BeeOnRope:您可能对基于 2000 年代硬件和软件的建议持怀疑态度,但我对不提供方法和数据的基准更加持怀疑态度。如果你想证明 `mmap` 更快,我希望至少看到整个测试设备(源代码)以及列表结果和处理器型号。 (3认同)
  • 我并不是想声称人们应该接受我的结果,而不是链接线程中呈现的结果。我的意思是_两者都_不够:人们应该在自己的系统上测试它,而不是简单地接受该线程的结果(该线程也没有提供详细的方法或数据)。我提供我的结果主要是为了指出我今天的结果与保罗当时的结果相反,作为呼吁人们在当地进行测试的一部分。非定量论证实际上只有在一种解决方案占主导地位时才具有说服力,但这里的情况并非如此。 (3认同)
  • @DietrichEpp-是的,我会精通TLB效果。注意,除非在特殊情况下,否则mmap不会刷新TLB(但是munmap可能)。我的测试包括了两个微基准测试(包括“ munmap”),也包括了在实际用例中运行的“在应用程序中”。当然,我的应用程序与您的应用程序不同,因此人们应该在本地进行测试。甚至还不清楚微基准测试是否支持mmap:read()也得到了很大的提升,因为用户端目标缓冲区通常位于L1中,这在大型应用程序中可能不会发生。是的,“很复杂”。 (2认同)

Tim*_*per 44

主要的性能成本是磁盘i/o."mmap()"肯定比istream快,但差异可能不明显,因为磁盘i​​/o将控制你的运行时间.

我试着本柯林斯的代码段(见上文/下文),以测试他断言,"mmap()的是方式更快",并没有发现可测量的差异.看看我对他的回答的评论.

除非你的"记录"很大,否则我肯定不会单独推荐mmap'ing每个记录 - 这将非常慢,每个记录需要2个系统调用,可能会丢失磁盘内存缓存中的页面.... .

在你的情况下,我认为mmap(),istream和低级open()/ read()调用都将大致相同.我建议在这些情况下使用mmap():

  1. 文件中存在随机访问(非顺序)AND
  2. 整个东西在内存中很舒服,或者文件中有引用位置,这样某些页面就可以映射到其他页面中.这样,操作系统就可以使用可用的RAM来获得最大的收益.
  3. 或者,如果多个进程正在读取/处理同一个文件,那么mmap()非常棒,因为这些进程都共享相同的物理页面.

(顺便说一句 - 我喜欢mmap()/ MapViewOfFile()).

  • 磁盘I/O方面应独立于访问方法.如果您真正随机访问大于RAM的文件,则mmap和seek + read都会严重受磁盘限制.否则两者都将受益于缓存.我认为文件大小与内存大小相比不是任何一个方向的强大参数.另一方面,文件大小与地址空间是一个非常强大的论据,特别是对于真正的随机访问. (3认同)
  • 我不会说文件必须舒适地放入内存,只放入地址空间。所以在 64 位系统上,应该没有理由不映射大文件。操作系统知道如何处理;它与用于交换的逻辑相同,但在这种情况下不需要额外的磁盘交换空间。 (2认同)

Ben*_*ins 42

MMAP是方式更快.您可以编写一个简单的基准来向自己证明:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}
Run Code Online (Sandbox Code Playgroud)

与:

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;
}
Run Code Online (Sandbox Code Playgroud)

很明显,我遗漏了细节(例如,如果你的文件不是文件的倍数,如何确定何时到达文件的末尾page_size),但它确实不应该比这复杂得多.

如果可以,您可以尝试将数据分解为多个文件,这些文件可以是mmap() - 整体而不是部分(更简单).

几个月前,我对boost_iostreams的滑动窗口mmap() - ed流类进行了半生不熟的实现,但没有人关心,我忙于其他的东西.最不幸的是,几个星期前我删除了一个旧的未完成项目的档案,这是受害者之一:-(

更新:我还应该添加一个警告,即这个基准测试在Windows中看起来会有很大的不同,因为Microsoft实现了一个漂亮的文件缓存,它首先完成了对mmap的大部分操作.即,对于频繁访问的文件,你可以只执行std :: ifstream.read()并且它将与mmap一样快,因为文件缓存已经为你做了内存映射,并且它是透明的.

最后更新:看,人们:在操作系统和标准库以及磁盘和内存层次结构的许多不同平台组合中,我无法肯定地说系统调用mmap(被视为黑盒子)始终总是快得多比read.这不完全是我的意图,即使我的话可以这样解释. 最后,我的观点是内存映射的i/o通常比基于字节的i/o更快; 这仍然是事实.如果你通过实验发现两者之间没有区别,那么对我来说唯一合理的解释就是你的平台在一种有利于调用性能的方式下实现了内存映射.read.绝对确定您以便携方式使用内存映射i/o的唯一方法是使用mmap.如果您不关心可移植性并且可以依赖目标平台的特定特性,那么使用read可能是合适的,而不会牺牲任何性能.

编辑以清理答案列表: @jbl:

滑动窗口mmap听起来很有趣.你能再谈一点吗?

当然 - 我正在为Git编写一个C++库(一个libgit ++,如果你愿意的话),我遇到了类似的问题:我需要能够打开大(非常大)的文件,而不是性能是一个完整的狗(就像它一样std::fstream).

Boost::Iostreams已经有一个mapped_file源,但问题是它是mmapping整个文件,这限制你2 ^(单词大小).在32位机器上,4GB不够大.期望.packGit中的文件变得比那些大得多并不是不合理的,所以我需要以块的形式读取文件而不需要使用常规文件i/o.在Boost::Iostreams我的封面下,我实现了一个Source,它或多或少地反映了std::streambuf和之间的相互作用std::istream.您也可以尝试类似的方法,只需继承std::filebuf到a mapped_filebuf,同样地,继承std::fstreama mapped_fstream.这是两者之间的相互作用,很难做到正确. Boost::Iostreams 有一些工作为你完成,它还提供了过滤器和链的钩子,所以我认为以这种方式实现它会更有用.

  • Ben,为什么要一次打扰`mmap()`文件?如果`size_t`足够容纳文件的大小(非常可能在64位系统上),那么只需`mmap()`一次调用整个文件. (11认同)
  • 亲爱的本:我看过那个链接.如果'mmap()'在Linux上速度不快,并且MapViewOfFile()在Windows上速度不快,那么你可以声称"mmap更快"吗?此外,由于理论上的原因,我认为顺序读取mmap()并不快 - 你有没有相反的解释? (9认同)
  • 我不同意接受的答案,但我相信这个答案是错误的.我按照你的建议在64位Linux机器上尝试了你的代码,mmap()并不比STL实现快.另外,理论上我不希望'mmap()'更快(或更慢). (6认同)
  • RE:Windows上的mmaped文件缓存.确切地说:当启用文件缓冲时,内核内存会映射您正在内部读取的文件,读入该缓冲区并将其复制回您的进程.这就好像你的内存自己映射它,除了额外的复制步骤. (3认同)
  • @Tim Cooper:你可能会发现这个主题(http://markmail.org/message/zbxnldcz6dlgo5of#query:mmap%20vs%20blocks+page:1+mid:zbxnldcz6dlgo5of+state:results).请注意以下两点:在Linux中没有对mmap进行适当优化,并且还需要在测试中使用madvise来获得最佳结果. (3认同)
  • @juhanic:你正在整理整个文件吗?性能优势部分来自于不重复系统调用. (3认同)
  • @Tim Cooper:我的测试可能不是很好的基准测试,因为使用istream.read()一次映射单个页面并一次读取一页的数据可能相当于同样的操作.我断言的原因是因为尝试两种方式的经验.例如,我编写了代码,我尝试打开非常大的文件进行阅读,并发现mmap是/ way /更快就像我声称的那样.一个更好的基准测试将测试不同的页面大小,不同的文件大小,不同的循环结构等.最后,我认为大多数人会发现mmap更快. (2认同)

Bee*_*ope 32

这里有许多好的答案,涵盖了许多重点,所以我只是添加一些我直接在上面没有看到的问题.也就是说,这个答案不应该被认为是综合的利弊,而是这里的其他答案的附录.

mmap看起来很神奇

假设文件已经完全缓存1作为基线2,mmap可能看起来非常像魔术:

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

在文件已经在缓存中的情况下,似乎无法击败:您只是直接访问内核页面缓存作为内存,它不会比这更快.

好吧,它可以.

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

mmap仍然可以进行每页工作

mmapvs的主要隐藏成本read(2)(实际上是用于读取块的 OS级别系统调用)是,mmap您需要为用户空间中的每个4K页面执行"一些工作",即使它可能被隐藏页面错误机制.

举个例子,一个典型的实现只mmap需要整个文件就可以进行故障,所以100 GB/4K = 2500万个故障就可以读取100 GB的文件.现在,这些都是小错误,但是250亿页面错误仍然不会超级快.在最好的情况下,轻微故障的成本可能在100s纳米.

mmap在很大程度上依赖于TLB性能

现在,您可以传递MAP_POPULATEmmap它以在返回之前设置所有页面表,因此访问它时应该没有页面错误.现在,这有一个小问题,它也将整个文件读入RAM,如果你试图映射一个100GB的文件,这将会爆炸 - 但现在让我们忽略它3.内核需要按页面工作来设置这些页面表(显示为内核时间).这最终成为该mmap方法的主要成本,并且它与文件大小成比例(即,随着文件大小的增长,它不会变得相对不那么重要)4.

最后,即使在用户空间访问中,这种映射也不是完全免费的(与不是源自基于文件的大型内存缓冲区相比mmap) - 即使一旦设置了页表,每次访问新页面都会进行,从概念上讲,招致TLB未命中.由于mmap文件意味着使用页面缓存及其4K页面,因此对于100GB文件,再次产生2500万次此成本.

现在,这些TLB未命中的实际成本在很大程度上取决于硬件的至少以下几个方面:(a)您拥有多少4K TLB内容以及其余的翻译缓存如何执行(b)硬件预取处理的程度如何使用TLB - 例如,可以预取触发页面遍历吗?(c)页面行走硬件的速度和平行程度.在现代高端x86英特尔处理器上,页面行走硬件通常非常强大:至少有2个并行页面步行器,页面遍历可以同时发生并继续执行,硬件预取可以触发页面遍历.因此,TLB 对流式读取负载的影响相当低 - 无论页面大小如何,此类负载通常都会执行相似的操作.但是,其他硬件通常要差得多!

read()避免了这些陷阱

read()系统调用,这是一般伏于"块读"类型提供例如,在C,C++等语言通话都有一个主要的缺点,每个人都充分认识到:

  • 每次read()调用N个字节必须将N个字节从内核复制到用户空间.

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

因此,基本上您可以进行以下比较,以确定单个读取大文件的速度更快:

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

在许多系统中,它们实际上是近似平衡的.请注意,每个扩展都具有完全不同的硬件和OS堆栈属性.

特别是,在以下情况下,该mmap方法变得相对更快

  • 操作系统具有快速的小故障处理功能,尤其是小故障扩展优化,例如故障排除.
  • 操作系统具有良好的MAP_POPULATE实现,可以在例如底层页面在物理内存中连续的情况下有效地处理大型映射.
  • 硬件具有强大的页面翻译性能,如大型TLB,快速二级TLB,快速和并行页面漫步,良好的预翻译与翻译等.

......在以下情况下,read()方法变得相对较快:

  • read()系统调用具有良好的复制性能.例如,copy_to_user内核方面的良好性能.
  • 内核具有映射内存的高效(相对于用户空间)方式,例如,仅使用几个具有硬件支持的大页面.
  • 内核具有快速的系统调用和一种在系统调用之间保持内核TLB条目的方法.

上述硬件因素在不同平台上变化很大,即使在同一系列中(例如,在x86代以内,特别是市场领域内),并且肯定跨越体系结构(例如,ARM vs x86与PPC).

OS因素也在不断变化,双方都有各种改进,导致一种方法或另一种方法的相对速度大幅上升.最近的清单包括:

  • 如上所述,增加了故障,这实际上有助于mmap没有MAP_POPULATE.
  • 添加快速路径copy_to_user方法arch/x86/lib/copy_user_64.S,例如,REP MOVQ在快速使用时,这确实有助于这种read()情况.

幽灵和熔化后更新

Spectre和Meltdown漏洞的缓解大大增加了系统调用的成本.在我测量的系统上,"无所事事"系统调用的成本(除了调用所做的任何实际工作之外,它是对系统调用的纯开销的估计)从典型的大约100 ns开始现代Linux系统大约700 ns.此外,根据您的系统,由于需要重新加载TLB条目,除了直接系统调用成本之外,专门针对Meltdown 的页表隔离修复还可能具有额外的下游效果.

read()基于方法相比,所有这都是基于方法的相对缺点mmap,因为read()方法必须对每个"缓冲区大小"的数据进行一次系统调用.你不能随意增加缓冲区大小来分摊这个成本,因为使用大缓冲区通常表现更差,因为你超过了L1大小,因此不断遭遇缓存未命中.

另一方面,使用mmap,您可以在一个大的内存区域中映射MAP_POPULATE并有效地访问它,但代价是只有一个系统调用.


1这或多或少还包括文件未完全缓存的情况,但操作系统预读足够好以使其显示如此(即,页面通常在您通过时缓存想要它).这是一个微妙的问题,因为预读的方式在mmapread呼叫之间通常是完全不同的,并且可以通过"建议"呼叫进一步调整,如2中所述.

2 ...因为如果文件没有被缓存,你的行为将完全由IO顾虑主导,包括你的访问模式对底层硬件的同情程度 - 你所有的努力都应该是确保这样的访问是同情的可能的,例如通过使用madvisefadvise调用(以及您可以进行的任何应用程序级别更改来改进访问模式).

3例如,您可以通过顺序mmap进入较小尺寸的窗口(例如100 MB)来解决这个问题.

4事实上,事实证明这种MAP_POPULATE方法(至少有一些硬件/操作系统组合)只比不使用它快一点,可能是因为内核使用了故障 - 因此实际的次要故障数减少了16倍或者.

  • 是的,这在很大程度上取决于场景.如果你读取_small够足够_并且随着时间的推移你倾向于重复读取相同的字节,`mmap`将具有不可逾越的优势,因为它避免了固定的内核调用开销.另一方面,`mmap`也会增加TLB压力,实际上对于"预热"阶段来说会更慢,因为在当前进程中第一次读取字节(尽管它们仍在页面页面中),因为它可能比"读取"做更多的工作,例如"故障"相邻的页面......对于相同的应用程序,"热身"就是最重要的!@CaetanoSauer (5认同)
  • 感谢您为这个复杂的问题提供更细微的答案.对大多数人来说,似乎很明显mmap更快,而实际上往往并非如此.在我的实验中,使用pread()随机访问带有内存索引的大型100GB数据库变得更快,尽管我为数百万次访问中的每一次访问都是malloc.它看起来像是业内人士[已观察到相同](https://github.com/facebook/rocksdb/issues/507). (4认同)

小智 7

对不起Ben Collins丢失了他的滑动窗口mmap源代码.在Boost中很高兴.

是的,映射文件要快得多.您实际上是使用操作系统虚拟内存子系统来关联内存到磁盘,反之亦然.以这种方式思考:如果操作系统内核开发人员可以更快地实现它.因为这样做可以更快地完成所有事情:数据库,启动时间,程序加载时间等等.

滑动窗口方法确实不是那么困难,因为可以同时映射多个连续页面.因此,只要任何单个记录中的最大记录适合内存,记录的大小就无关紧要.重要的是管理簿记.

如果记录未在getpagesize()边界上开始,则映射必须从上一页开始.映射区域的长度从记录的第一个字节(如果需要向下舍入到最接近的getpagesize()的倍数)延伸到记录的最后一个字节(向上舍入到最接近的getpagesize()的倍数).处理完记录后,可以取消映射(),然后继续处理.

使用CreateFileMapping()和MapViewOfFile()(和GetSystemInfo()来获取SYSTEM_INFO.dwAllocationGranularity ---而不是SYSTEM_INFO.dwPageSize),这在Windows下也可以正常工作.


Leo*_*ans 5

mmap 应该更快,但我不知道多少。这在很大程度上取决于您的代码。如果您使用 mmap,最好一次对整个文件进行 mmap,这将使您的生活更轻松。一个潜在的问题是,如果您的文件大于 4GB(或者实际上限制更低,通常为 2GB),您将需要 64 位架构。因此,如果您使用的是 32 位环境,您可能不想使用它。

话虽如此,可能有更好的方法来提高性能。你说输入文件被扫描了很多次,如果你可以一次读取它然后完成它,那可能会快得多。