mmap() vs. reading blocks

21 浏览
0 Comments

mmap() vs. reading blocks

我正在开发一个程序,用于处理可能达到100GB或更大的文件。这些文件包含不定长的记录集。我已经完成了第一个实现,并希望改善性能,特别是在I/O方面更高效,因为输入文件需要多次扫描。

在使用C++的fstream库以块的方式读取和使用mmap()之间,有什么经验法则吗?我想做的是将大块数据从磁盘读入缓冲区,从缓冲区处理完整的记录,然后再读取更多数据。

使用mmap()的代码可能会变得非常混乱,因为mmap()的块需要位于页面大小的边界上(据我所知),而记录可能横跨页面边界。使用fstream,我可以直接定位到记录的起始位置并重新开始读取,因为我们不限于只读取位于页面大小边界上的块。

如何在没有完整实现之前决定使用哪种选项?有什么经验法则(例如,mmap()速度是2倍)或简单的测试方法吗?

0
0 Comments

mmap() vs. reading blocks:哪个更快?

在讨论mmap()和reading blocks的优缺点之前,我们先来看一下mmap()的一些特点:

1. mmap()只需要进行一次系统调用来(可能)映射整个文件,之后就不需要再进行系统调用了。

2. mmap()不需要将文件数据从内核复制到用户空间。

3. mmap()允许您将文件“作为内存”访问,包括使用内存的各种高级技巧,如编译器自动向量化、SIMD指令、预取、优化的内存解析例程、OpenMP等。

在文件已经完全缓存的情况下,mmap()似乎不可战胜:您可以直接将内核页缓存作为内存进行访问,速度不可能比这更快。但实际上,还是可以有更快的方法。

mmap()并不是真正的“魔法”,因为...

1. mmap()与read()相比(read()是用于读取块的系统调用),在每个4K页面上仍然需要进行“一些工作”,即使这可能被页面错误机制隐藏起来。对于只是将整个文件映射为内存的典型实现来说,读取一个100GB文件需要引发2500万个页面错误。虽然这些页面错误是“次要错误”,但2500万个页面错误仍然不会非常快。页面错误的成本可能在最佳情况下为几百纳秒。

2. mmap()在很大程度上依赖于TLB的性能。您可以向mmap()传递MAP_POPULATE参数,告诉它在返回之前设置好所有的页表,因此在访问它时不会出现页面错误。然而,这也会带来一个小问题,即它会将整个文件读入RAM中,如果尝试映射一个100GB的文件,这将会导致内存溢出问题。内核需要对这些页表进行逐页工作(显示为内核时间)。这最终成为mmap()方法中的一个主要成本,并且与文件大小成正比(即随着文件大小的增长,它的重要性不会相对减少)。

3. 即使在用户空间访问这样的映射也并非完全免费(与不源自基于文件的mmap的大内存缓冲区相比)-即使在设置好页表后,每次访问一个新的页面,从概念上讲,都会产生一个TLB缺失。由于mmap()文件意味着使用页面缓存及其4K页面,所以对于一个100GB的文件,您将再次承担这个成本2500万次。

read()避免了这些问题。read()系统调用是C、C++和其他语言中提供的“块读取”类型调用的基础,它的主要缺点是每次读取N个字节时都需要将N个字节从内核复制到用户空间。然而,它避免了上述大部分成本-您不需要将2500万个4K页面映射到用户空间。通常可以在用户空间中malloc一个小缓冲区,并将其重复用于所有的read()调用。在内核端,由于整个RAM通常使用一些非常大的页面(例如x86上的1GB页面)进行线性映射,所以几乎没有4K页面或TLB缺失的问题,因此页面缓存中的底层页面在内核空间中被有效地覆盖。

所以,对于从大文件中进行一次读取而言,以下比较可以决定哪个更快:

“mmap”方法中隐含的每页工作量是否比使用“read()”导致的内核到用户空间复制文件内容的每字节工作量更高?

在许多系统上,它们实际上是大致平衡的。需要注意的是,它们每一个都会根据硬件和操作系统的完全不同属性进行扩展。

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

1. 操作系统具有快速的次要错误处理和次要错误批处理优化(例如,fault-around)。

2. 操作系统具有良好的MAP_POPULATE实现,可以在底层页面在物理内存中连续的情况下高效地处理大型映射。

3. 硬件具有强大的页转换性能,例如大型TLB、快速的第二级TLB、快速且并行的页行走器、与TLB的高效预取交互等。

而read()方法在以下情况下相对更快:

1. read()系统调用具有较好的复制性能,例如内核端的优秀copy_to_user性能。

2. 内核具有相对于用户空间而言高效的内存映射方式,例如只使用少量大页面和硬件支持。

3. 内核具有快速的系统调用和一种在系统调用之间保持内核TLB条目的方法。

上述硬件因素在不同平台上变化很大,甚至在同一系列中(例如,在x86的不同代和特定市场段之间),在不同体系结构(例如,ARM vs x86 vs PPC)之间也有很大差异。

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

1. 引入了fault-around,如上所述,这对于不使用MAP_POPULATE的mmap()方法非常有帮助。

2. 在arch/x86/lib/copy_user_64.S中添加了快速copy_to_user方法,例如在复制速度较快时使用REP MOVQ,这对于read()方法非常有帮助。

在Spectre和Meltdown漏洞的修复之后,系统调用的成本大大增加。在我测量过的系统上,一个“do nothing”系统调用(这是一个纯粹的系统调用开销估计,不包括调用执行的任何实际工作)的成本,从典型的现代Linux系统上的约100纳秒增加到约700纳秒。此外,根据您的系统,专门针对Meltdown的页表隔离修复可能会产生额外的下游效应,除了直接系统调用成本之外,还需要重新加载TLB条目。

所有这些对于基于read()的方法来说相对不利,因为每个“缓冲区大小”的数据都必须进行一次系统调用。您不能任意增加缓冲区大小来摊销这个成本,因为使用大缓冲区通常性能更差,因为您超出了L1的大小,因此不断遭受缓存未命中。

另一方面,通过mmap(),您可以使用MAP_POPULATE将大块内存映射到内存中,并且只需要进行一次系统调用的成本。

要决定哪种方法对于从大文件中进行一次读取来说更快,需要根据具体的场景进行比较。不同的应用场景可能有不同的结果。例如,如果读取的大小足够小,并且随着时间的推移,您往往会重复读取相同的字节,那么mmap()将具有不可逾越的优势,因为它避免了固定的内核调用开销。另一方面,mmap()还会增加TLB的压力,并且在当前进程中的“热身”阶段,即首次读取当前进程中的字节时,可能比read()更慢,因为它可能需要比read()更多的工作,例如“围绕错误”相邻页面...对于某些应用来说,只有“热身”才是最重要的!

还有一个需要注意的是,在第二段的最后一个句子中,当提到“25 billion page faults”时,应更正为“25 million page faults”。我不能100%确定,所以不直接进行编辑。

0
0 Comments

mmap()和reading blocks之间的性能差异是一个经典问题。在Linux内核邮件列表的一篇帖子中,解释了mmap()和reading blocks性能差异的原因。虽然这篇帖子是在2000年发布的,但是自那时以来,内核的IO和虚拟内存方面已经有了许多改进。文章指出,mmap()调用相对于reading blocks具有更高的开销,原因是虚拟内存映射操作在某些处理器上的代价很高。同时,IO系统已经可以使用磁盘缓存,无论使用哪种方法读取文件,都会命中或未命中缓存。

然而,对于随机访问,内存映射通常更快,特别是在访问模式稀疏和不可预测的情况下。内存映射允许您在使用完毕之前一直使用缓存中的页面。这意味着,如果您长时间频繁地使用一个文件,然后关闭它并重新打开它,页面仍将被缓存。而使用reading blocks,则可能导致文件在缓存中被清除。当然,如果您仅使用文件并立即丢弃它,则此情况不适用。直接读取文件非常简单快速。

文章还提到了Java程序员对非阻塞I/O比阻塞I/O慢的震惊,以及其他网络程序员对epoll比poll慢的震惊。这些现象的原因与mmap()和reading blocks类似,都是因为非阻塞I/O和epoll需要进行更多的系统调用,从而导致了更高的开销。

总之,如果数据访问是随机的,并且需要长时间保留或与其他进程共享,那么使用内存映射是一个不错的选择。如果数据访问是顺序的或者在读取后丢弃,那么使用reading blocks即可。如果某种方法使您的程序更简单,请使用该方法。对于许多真实场景,没有确切的方法可以在没有测试实际应用程序的情况下确定哪种方法更快。

需要注意的是,基于2000年的硬件和软件的建议,没有经过今天的测试,可能是一个非常可疑的方法。此外,尽管该帖子中关于mmap()和read()的大多数事实仍然是正确的,但是整体性能实际上只能通过在特定硬件配置上进行测试来确定。例如,"A call to mmap has more overhead than read"这一说法是值得商榷的-是的,mmap()必须将映射添加到进程页表,但read()必须将所有读取的字节从内核复制到用户空间。

对于我的硬件(现代的Intel,大约是2018年),mmap()在大于页面大小(4 KiB)的读取中具有比read()更低的开销。现在很明显,如果要稀疏随机访问数据,mmap()非常好,但反过来并不一定成立:即使对于顺序访问,mmap()可能仍然是最佳选择。

需要注意的是,在测试这种内存系统的情况下,微基准测试可能非常具有欺骗性,因为TLB刷新可能会对程序的其他部分性能产生负面影响,如果只测量mmap()本身,则不会显示出这种影响。

对于基于字节可寻址持久内存(如Optane DCPMM)的新技术,我真的很想知道它与当前情况的一致性如何。

0
0 Comments

mmap() vs. 读取数据块

在处理性能方面,主要的成本在于磁盘I/O。虽然"mmap()"比istream更快,但由于磁盘I/O会占据运行时间的主导地位,所以这种差异可能并不明显。

为了测试对"mmap()非常快速"这一说法的断言,我尝试了Ben Collins的代码片段,并没有发现明显的差异。请参考我对他回答的评论。

除非你的"records"非常庞大,否则我绝对不建议逐个映射每个记录,这样会非常慢,每个记录都需要两次系统调用,可能会导致磁盘内存缓存的页面被换出......

在你的情况下,我认为mmap()、istream和底层的open()/read()调用都差不多。在以下情况下,我建议使用mmap():

1. 文件中存在随机访问(非顺序访问);

2. 整个文件舒适地适应内存空间,或者文件中存在局部引用,使得某些页面可以映射进来,其他页面映射出去。这样操作系统可以最大程度地利用可用的内存;

3. 或者多个进程正在读取/处理相同的文件,这时mmap()非常好用,因为这些进程共享同一物理页面。

顺便说一句,我喜欢mmap()/MapViewOfFile()。

关于随机访问的观点很有道理,这可能是我感知到的其中一个因素。

我不会说文件必须舒适地适应内存,只要适应地址空间就可以了。所以在64位系统上,没有理由不映射大文件。操作系统知道如何处理这种情况,这与交换的逻辑相同,但在这种情况下不需要额外的磁盘交换空间。

你明白关于磁盘I/O的观点吗?如果文件适应地址空间但不适应内存,并且存在随机访问,那么每次记录访问都可能需要磁头移动和寻道操作,或者SSD页面操作,这对性能来说是一场灾难。

磁盘I/O方面应该与访问方法无关。如果你对大于RAM的文件进行真正的随机访问,无论是mmap还是seek+read都会严重受限于磁盘。否则,两者都将从缓存中受益。我不认为文件大小与内存大小之间有明显的偏向。另一方面,文件大小与地址空间之间是一个非常强的论点,尤其是对于真正的随机访问。

我最初的答案和现在仍然有这个观点:"整个文件舒适地适应内存,或者文件中存在局部引用"。所以第二点回答了你的观点。

0