这里已经有很多很好的答案涵盖了许多要点,所以我将添加几个我没有直接在上面看到的问题。也就是说,这个答案不应该被认为是利弊的综合,而是这里其他答案的补充。
mmap 看起来很神奇
以文件已经完全缓存的情况1作为基线2,mmap
可能看起来很像魔术:
mmap
只需要 1 次系统调用(可能)映射整个文件,之后不再需要系统调用。
mmap
不需要将文件数据从内核复制到用户空间。
mmap
允许您“作为内存”访问文件,包括使用您可以对内存执行的任何高级技巧对其进行处理,例如编译器自动矢量化、SIMD内在函数、预取、优化的内存解析例程、OpenMP 等。
在文件已经在缓存中的情况下,似乎无法击败:您只是直接将内核页面缓存作为内存访问,并且无法比这更快。
嗯,它可以。
mmap 实际上并不神奇,因为...
mmap 仍然可以按页面工作
mmap
vs的一个主要隐藏成本read(2)
(实际上是用于读取块的可比较的操作系统级系统调用)是,mmap
您需要为在新映射中访问的每个 4K 页面做“一些工作”,即使它可能被隐藏页面错误机制。
举个例子,一个典型的实现,只mmap
需要整个文件就需要故障输入,所以 100 GB / 4K = 2500 万次故障才能读取 100 GB 文件。现在,这些将是小错误,但 2500 万页错误仍然不会超快。在最好的情况下,一个小故障的成本可能在 100 纳米。
mmap 严重依赖 TLB 性能
现在,您可以通过MAP_POPULATE
tommap
告诉它在返回之前设置所有页表,因此在访问它时应该没有页面错误。现在,这有一个小问题,它还将整个文件读入 RAM,如果您尝试映射 100GB 文件,这将会爆炸 - 但现在让我们忽略它3。内核需要做每页的工作来设置这些页表(显示为内核时间)。这最终成为该mmap
方法的主要成本,并且与文件大小成正比(即,随着文件大小的增长,它不会变得相对不那么重要)4。
最后,即使在用户空间访问这样的映射也不是完全免费的(与不是源自基于文件的大内存缓冲区相比mmap
) - 即使设置了页表,每次访问新页面都会,从概念上讲,会导致 TLB 未命中。由于mmap
读取文件意味着使用页面缓存及其 4K 页面,因此对于 100GB 的文件,您再次需要花费 2500 万次。
现在,这些 TLB 未命中的实际成本在很大程度上取决于至少以下硬件方面:(a) 你有多少 4K TLB 实体以及翻译缓存的其余部分如何执行 (b) 硬件预取处理的性能如何使用 TLB - 例如,预取可以触发页面遍历吗?(c) 页面遍历硬件的速度和并行度。在现代高端 x86 Intel 处理器上,page walk 硬件通常非常强大:至少有 2 个并行 page walker,page walk 可以与继续执行同时发生,并且硬件预取可以触发 page walk。因此,TLB 对流式读取负载的影响相当低——无论页面大小如何,这种负载通常都会执行类似的操作。但是,其他硬件通常要差得多!
read() 避免了这些陷阱
系统read()
调用通常是“块读取”类型调用的基础,例如,在 C、C++ 和其他语言中,它有一个每个人都清楚的主要缺点:
- 每次
read()
调用 N 个字节都必须将 N 个字节从内核复制到用户空间。
另一方面,它避免了上述大部分成本——您不需要将 2500 万个 4K 页面映射到用户空间。您通常可以在用户空间中使用单个缓冲区小缓冲区,并在所有调用malloc
中重复使用它。read
在内核方面,4K 页面或 TLB 未命中几乎没有问题,因为所有 RAM 通常使用几个非常大的页面(例如,x86 上的 1 GB 页面)进行线性映射,因此页面缓存中的底层页面被覆盖在内核空间中非常有效。
所以基本上你有以下比较来确定单次读取大文件的速度更快:
该方法隐含的每页额外工作是否mmap
比使用隐含的将文件内容从内核复制到用户空间的每字节工作成本更高read()
?
在许多系统上,它们实际上是近似平衡的。请注意,每一个都具有完全不同的硬件和操作系统堆栈属性。
特别是,在以下mmap
情况下,该方法变得相对更快:
- 该操作系统具有快速的次要故障处理,尤其是次要故障批量优化,例如故障处理。
- 操作系统有一个很好的
MAP_POPULATE
实现,可以有效地处理大型映射,例如,底层页面在物理内存中是连续的。
- 硬件具有强大的页面翻译性能,如大型TLB、快速的二级TLB、快速并行的page-walker、良好的预取与翻译交互等。
...而这种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 后更新
Spectre 和 Meltdown 漏洞的缓解措施大大增加了系统调用的成本。在我测量过的系统上,“什么都不做”系统调用的成本(这是对系统调用的纯开销的估计,除了调用所做的任何实际工作)从典型的大约 100 ns现代Linux系统约700纳秒。此外,根据您的系统,专门针对 Meltdown 的页表隔离修复可能会产生额外的下游影响,除了由于需要重新加载 TLB 条目而导致的直接系统调用成本。
read()
与基于方法相比,所有这些都是基于方法的相对劣势mmap
,因为read()
方法必须为每个“缓冲区大小”的数据进行一次系统调用。您不能随意增加缓冲区大小来分摊此成本,因为使用大缓冲区通常会执行更差,因为您超过了 L1 大小,因此经常遭受缓存未命中。
另一方面,使用mmap
,您可以在大内存区域中进行映射MAP_POPULATE
并有效地访问它,而只需一次系统调用。
1这或多或少还包括文件未完全缓存的情况,但操作系统的预读足以使其显示为想要它)。这是一个微妙的问题,因为预读的工作方式在mmap
andread
调用之间通常非常不同,并且可以通过“advise”调用进一步调整,如2中所述。
2 ...因为如果文件没有被缓存,你的行为将完全被 IO 问题所支配,包括你的访问模式对底层硬件的同情程度——你所有的努力都应该确保这种访问与可能的,例如通过使用madvise
orfadvise
调用(以及您可以进行的任何应用程序级别更改以改进访问模式)。
3例如,您可以mmap
通过在较小的窗口(例如 100 MB)中顺序读取来解决此问题。
4事实上,事实证明,这种MAP_POPULATE
方法(至少一个硬件/操作系统组合)只比不使用它稍微快一点,可能是因为内核正在使用faultaround - 所以实际的小故障数量减少了 16 倍或者。