这里已经有很多很好的答案,涵盖了很多要点,所以我只添加几个我没有直接在上面解决的问题。也就是说,此答案不应被认为是全面的优缺点,而应视为此处其他答案的附录。
mmap看起来像魔术
以文件已经被完全缓存1为基线2的情况下,mmap
看起来很像魔术:
mmap
仅需要1个系统调用即可(可能)映射整个文件,此后不再需要任何系统调用。
mmap
不需要将文件数据从内核复制到用户空间。
mmap
允许您“作为内存”访问文件,包括使用您可以针对内存执行的任何高级技巧对其进行处理,例如编译器自动矢量化,SIMD内在函数,预取,优化的内存解析例程,OpenMP等。
如果文件已经在高速缓存中,则似乎无法克服:您只是直接访问内核页面高速缓存作为内存,而且它的速度不能超过此速度。
好吧,可以。
mmap实际上不是魔术,因为...
mmap仍可以按页面工作
mmap
vs的主要隐藏成本read(2)
(实际上是与读取块相对应的OS级syscall )是,mmap
您需要为用户空间中的每个4K页面做一些“工作”,即使它可能被用户空间隐藏了。页面故障机制。
例如,仅mmap
整个文件的典型实现将需要进行故障处理,因此100 GB / 4K = 2500万个故障才能读取100 GB的文件。现在,这些将是次要的错误,但是250亿页的错误仍然不会很快。在最佳情况下,一次小故障的成本可能约为100纳米。
mmap严重依赖TLB性能
现在,你可以传递MAP_POPULATE
给mmap
告诉它在返回前设置所有的页表,所以在访问它不应该有页面错误。现在,这有一个小问题,它也将整个文件读入RAM,如果您尝试映射100GB的文件,该文件将被炸毁-但现在我们忽略它3。内核需要完成每页的工作才能设置这些页表(显示为内核时间)。这最终成为该mmap
方法的主要成本,并且与文件大小成正比(即,随着文件大小的增加,它并不会变得相对不那么重要)4。
最后,即使在用户空间中访问,这种映射也不是完全免费的(与并非源自基于文件的大内存缓冲区相比mmap
)-即使建立了页表,每次对新页的访问都将到达,从概念上讲,会导致TLB错过。由于mmap
写入文件意味着使用页面缓存及其4K页面,因此对于100GB的文件,您再次要花费2500万次。
现在,这些TLB缺失的实际成本在很大程度上至少取决于硬件的以下方面:(a)您拥有多少4K TLB实体以及其余的转换缓存如何工作(b)硬件预取处理得如何好使用TLB-例如,预取能否触发页面浏览?(c)分页浏览硬件的速度和并行度。在现代高端x86 Intel处理器上,页面浏览硬件通常非常强大:至少有2个并行页面浏览器,页面浏览可以与连续执行同时发生,并且硬件预取可以触发页面浏览。因此,TLB 对流读取负载的影响非常小-且无论页面大小如何,这种负载通常都将以类似的方式执行。但是,其他硬件通常要差得多!
read()避免了这些陷阱
该read()
系统调用,这是一般伏于“块读”呼叫类型提供例如,在C,C ++和其他语言有一个主要的缺点,每个人都充分认识到:
- 每次
read()
调用N个字节必须将N个字节从内核复制到用户空间。
另一方面,它避免了上述大部分成本-您无需将2500万个4K页面映射到用户空间。通常,您可以malloc
在用户空间中使用单个缓冲区或小缓冲区,然后将其重复用于所有read
调用。在内核方面,4K页或TLB丢失几乎没有问题,因为通常使用几个非常大的页(例如,x86上的1 GB页)线性映射所有RAM,因此覆盖了页缓存中的基础页在内核空间中非常有效。
因此,基本上,您可以通过下面的比较来确定一次读取大文件的速度更快:
这种mmap
方法隐含的额外的每页工作是否比使用隐含的将文件内容从内核复制到用户空间的每字节工作更昂贵read()
?
在许多系统上,它们实际上是近似平衡的。请注意,每个扩展的硬件和操作系统堆栈的属性完全不同。
特别是在以下情况下,该mmap
方法变得相对较快:
- 该操作系统具有快速的轻微故障处理功能,尤其是诸如故障排除之类的轻微故障批量优化。
- OS具有良好的
MAP_POPULATE
实现,可以在例如基础页面在物理内存中连续的情况下有效处理大型地图。
- 硬件具有强大的页面翻译性能,例如大型TLB,快速的第二级TLB,快速和并行的页面遍历器,与翻译的良好预取交互等。
...虽然在以下情况下该read()
方法变得相对较快:
- 该
read()
系统调用具有良好的复制性能。例如,copy_to_user
内核方面的良好性能。
- 内核具有一种有效的(相对于用户态)映射内存的方式,例如,仅使用几个带有硬件支持的大页面。
- 内核具有快速的系统调用,并且可以在整个系统调用之间保留内核TLB条目。
上面的硬件因素在不同平台之间,甚至在同一家族中(例如,在x86代之内,尤其是细分市场),以及在不同体系结构(例如,ARM与x86与PPC)之间,差异都很大。
操作系统因素也不断变化,双方的各种改进都导致一种方法或另一种方法的相对速度大幅提高。最近的列表包括:
- 如上所述,增加了故障排除功能,对
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这种或多或少的情况还包括文件没有被完全缓存到开始的情况,但是预读操作系统足以使文件看起来像这样(例如,页面通常在您存储时被缓存)。想要它)。但是,这是一个微妙的问题,因为预调用的工作方式在mmap
和read
调用之间通常大不相同,并且可以通过2中所述的“建议”调用进一步调整。
2 ...因为如果不缓存文件,那么您的行为将完全由IO问题决定,包括您对底层硬件的访问模式有多同情-您应尽一切努力确保此类访问具有同情心例如,通过使用madvise
或fadvise
调用(以及可以对应用程序级别进行任何更改以改善访问模式)。
3例如,您可以mmap
通过在较小尺寸(例如100 MB)的窗口中依次打开来解决此问题。
4实际上,事实证明,该MAP_POPULATE
方法(至少是某些硬件/操作系统组合中的一种)仅比不使用它要快一点,这可能是因为内核正在使用故障排除方法 -因此实际的次要故障数减少了16倍或者。
mmap()
中,比使用syscalls快2-6倍read()
。