ChatGPT解决这个技术问题 Extra ChatGPT

mmap() 与读取块

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

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

mmap() 代码可能会变得非常混乱,因为 mmap 的块需要位于页面大小的边界上(我的理解),并且记录可能会跨越页面边界。使用 fstream,我可以只寻找记录的开头并重新开始阅读,因为我们不限于阅读位于页面大小边界上的块。

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

这是一篇有趣的文章:medium.com/@sasha_f/… 在实验中,mmap() 比使用系统调用(例如 read())快 2-6 倍。

M
MvG

我试图找到关于 Linux 上 mmap / 读取性能的最终结论,我在 Linux 内核邮件列表上发现了一篇不错的帖子 (link)。它是从 2000 年开始的,因此从那时起内核中的 IO 和虚拟内存有了很多改进,但它很好地解释了 mmapread 可能更快或更慢的原因。

对 mmap 的调用比 read 开销更大(就像 epoll 比 poll 开销更大,poll 比 read 开销更大)。更改虚拟内存映射在某些处理器上是一项相当昂贵的操作,原因与在不同进程之间切换成本高昂的原因相同。

IO系统已经可以使用磁盘缓存,所以如果你读取一个文件,无论你使用什么方法,你都会命中或错过缓存。

然而,

对于随机访问,内存映射通常更快,尤其是在您的访问模式稀疏且不可预测的情况下。

内存映射允许您继续使用缓存中的页面,直到完成。这意味着如果您长时间大量使用文件,然后将其关闭并重新打开,页面仍然会被缓存。通过读取,您的文件可能在很久以前就已从缓存中刷新。如果您使用文件并立即丢弃它,则不适用。 (如果您尝试 mlock 页面只是为了将它们保留在缓存中,那么您就是在试图智取磁盘缓存,而这种愚蠢的做法很少有助于系统性能)。

直接读取文件非常简单快捷。

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

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

其他一些网络程序员惊讶地发现 epoll 通常比 poll 慢,如果您知道管理 epoll 需要进行更多的系统调用,这完全有道理。

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

(对不起,这个问题被删除了,但我一直在寻找答案,这个问题一直出现在谷歌搜索结果的顶部。)


请记住,使用任何基于 2000 年代硬件和软件的建议,而今天不对其进行测试将是一种非常可疑的方法。此外,虽然该线程中关于 mmapread() 的许多事实仍然像过去一样真实,但整体性能并不能真正通过将优缺点相加来确定,而只能通过在特定的硬件配置。例如,“对 mmap 的调用比读取的开销更大”是有争议的——是的 mmap 必须将映射添加到进程页表,但 read 必须将所有读取的字节从内核复制到用户空间。
结果是,在我的(现代英特尔,大约 2018 年)硬件上,对于大于页面大小 (4 KiB) 的读取,mmap 的开销低于 read。现在确实,如果您想以稀疏和随机的方式访问数据,mmap 确实非常非常好 - 但反过来不一定是正确的:mmap 可能仍然是顺序访问的最佳选择。
@BeeOnRope:您可能对基于 2000 年代硬件和软件的建议持怀疑态度,但我对不提供方法和数据的基准更加持怀疑态度。如果您想证明 mmap 更快,我希望至少能看到带有表格结果的整个测试设备(源代码)和处理器型号。
我并不是要声称人们应该接受我的结果,而不是链接线程中提供的结果。我的意思是两者都不够:人们应该在自己的系统上对其进行测试,而不是简单地接受来自该线程的结果(该线程也没有提供详细的方法或数据)。我提供我的结果主要是为了指出我今天的结果与保罗当时的结果相反,作为呼吁人们在当地进行测试的一部分。非定量论据只有在一个解决方案支配另一个解决方案时才真正令人信服,而这里的情况并非如此。
@DietrichEpp - 是的,我会精通 TLB 效果。请注意,mmap 不会刷新 TLB,除非在异常情况下(但 munmap 可能)。我的测试包括微基准测试(包括 munmap,还包括在实际用例中运行的“应用程序中”。当然我的应用程序和你的应用程序不一样,所以人们应该在本地测试。甚至不清楚 mmap 是否受到微基准的青睐:read() 也得到了很大的提升,因为用户端目标缓冲区通常停留在 L1 中,这在更大的应用程序中可能不会发生。所以,是的,“这很复杂”。
B
BeeOnRope

这里已经有很多很好的答案涵盖了许多要点,所以我将添加几个我没有直接在上面看到的问题。也就是说,这个答案不应该被认为是利弊的综合,而是这里其他答案的补充。

mmap 看起来很神奇

以文件已经完全缓存的情况1 作为基线2mmap 可能看起来很像 magic

mmap 只需要 1 次系统调用来(可能)映射整个文件,之后不再需要系统调用。 mmap 不需要将文件数据从内核复制到用户空间。 mmap 允许您“作为内存”访问文件,包括使用您可以对内存执行的任何高级技巧对其进行处理,例如编译器自动矢量化、SIMD 内在函数、预取、优化的内存解析例程、OpenMP 等。

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

嗯,它可以。

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

mmap 仍然可以按页面工作

mmapread(2) 的主要隐藏成本(这实际上是 读取块 的可比较的操作系统级系统调用)是使用 mmap 您需要为在新映射中访问的每个 4K 页面,即使它可能被页面错误机制隐藏。

举个例子,一个典型的实现只需要 mmaps 整个文件将需要故障输入,因此 100 GB / 4K = 2500 万次故障才能读取 100 GB 文件。现在,这些将是 minor faults,但是 2500 万个页面错误仍然不会很快。在最好的情况下,一个小故障的成本可能在 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 处理器上,page walk 硬件通常非常强大:至少有 2 个并行 page walker,page walk 可以与继续执行同时发生,并且硬件预取可以触发 page walk。因此,TLB 对流式读取负载的影响相当低 - 无论页面大小如何,这种负载通常都会执行类似的操作。但是,其他硬件通常要差得多!

read() 避免了这些陷阱

read() 系统调用通常是“块读取”类型调用的基础,例如,在 C、C++ 和其他语言中,它有一个每个人都清楚的主要缺点:

每个 N 字节的 read() 调用都必须将 N 字节从内核复制到用户空间。

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

所以基本上你有以下比较来确定单次读取大文件的速度更快:

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

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

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

该操作系统具有快速的次要故障处理,尤其是次要故障批量优化,例如故障处理。

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

硬件具有强大的页面翻译性能,如大型TLB、快速的二级TLB、快速并行的page-walkers、良好的预取与翻译交互等。

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

read() 系统调用具有良好的复制性能。例如,内核端良好的 copy_to_user 性能。

内核有一种有效的(相对于用户空间)映射内存的方式,例如,只使用几个有硬件支持的大页面。

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

上述硬件因素在不同平台之间差异很大,甚至在同一个系列中(例如,在 x86 代内,尤其是在细分市场中),并且肯定会跨架构(例如,ARM 与 x86 与 PPC)。

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

如上所述,添加了故障解决方案,这确实有助于没有 MAP_POPULATE 的 mmap 情况。

在 arch/x86/lib/copy_user_64.S 中添加快速路径 copy_to_user 方法,例如,在快速时使用 REP MOVQ,这确实有助于 read() 情况。

Spectre 和 Meltdown 后更新

Spectre 和 Meltdown 漏洞的缓解措施大大增加了系统调用的成本。在我测量过的系统上,“什么都不做”系统调用的成本(这是对系统调用的纯开销的估计,除了调用完成的任何实际工作)从典型的大约 100 ns现代Linux系统约700纳秒。此外,根据您的系统,除了需要重新加载 TLB 条目的直接系统调用成本之外,专门针对 Meltdown 的 page-table isolation 修复可能会产生额外的下游影响。

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

另一方面,使用 mmap,您可以使用 MAP_POPULATE 映射到大的内存区域并有效地访问它,而只需一次系统调用。

1 这或多或少也包括文件没有完全缓存开始的情况,但操作系统预读足够好使其看起来如此(即页面通常在您想要的时候缓存)。这是一个微妙的问题,因为预读的工作方式在 mmapread 调用之间通常有很大不同,并且可以通过 2 中所述的“建议”调用进一步调整。

2 ...因为如果文件没有缓存,您的行为将完全由 IO 问题主导,包括您的访问模式对底层硬件的同情程度 -并且您应尽最大努力确保此类访问尽可能具有同情心,例如通过使用 madvisefadvise 调用(以及您可以进行的任何应用程序级别更改以改进访问模式)。

3 例如,您可以通过顺序mmap在较小尺寸的窗口(例如 100 MB)中解决此问题。

4 事实上,事实证明 MAP_POPULATE 方法(至少是某种硬件/操作系统组合)只比不使用它稍微快一点,可能是因为内核正在使用 faultaround - 所以小故障的实际数量减少了 16 倍左右。


感谢您为这个复杂的问题提供更细致入微的答案。大多数人似乎很明显 mmap 更快,但实际上并非如此。在我的实验中,使用内存索引随机访问一个 100GB 的大型数据库结果证明使用 pread() 更快,即使我为数百万次访问中的每一个都分配了一个缓冲区。似乎业内有很多人have observed the same
是的,很大程度上取决于场景。如果您的读取足够小并且随着时间的推移您倾向于重复读取相同的字节,则 mmap 将具有不可逾越的优势,因为它避免了固定的内核调用开销。另一方面,mmap 也增加了 TLB 压力,实际上使得在当前进程中第一次读取字节的“预热”阶段变慢(尽管它们仍在页面页面中),因为它可能比 read 做更多的工作,例如“故障排除”相邻页面......对于相同的应用程序,“热身”才是最重要的! @CaetanoSauer
我认为您所说的“......但是 250 亿个页面错误仍然不会超快......”它应该是“......但是 2500 万个页面错误仍然不会超快......” .我不是 100% 肯定的,所以这就是我不直接编辑的原因。
T
Tim Cooper

主要的性能成本将是磁盘 i/o。 "mmap()" 肯定比 istream 快,但差异可能并不明显,因为磁盘 i/o 将支配您的运行时间。

我尝试了 Ben Collins 的代码片段(见上/下)来测试他关于“mmap() 速度更快”的断言,但没有发现可测量的差异。请参阅我对他的回答的评论。

我当然不建议依次单独对每条记录进行映射,除非您的“记录”很大 - 这将非常慢,每条记录需要 2 次系统调用,并且可能会从磁盘内存缓存中丢失页面...... .

在您的情况下,我认为 mmap()、istream 和低级 open()/read() 调用都差不多。在这些情况下,我会推荐 mmap():

文件中存在随机访问(非顺序),并且整个内容可以舒适地放入内存中,或者文件中存在引用位置,以便可以映射某些页面并映射出其他页面。这样,操作系统就可以使用可用的 RAM 来获得最大收益。或者,如果多个进程正在读取/处理同一个文件,那么 mmap() 非常棒,因为这些进程都共享相同的物理页面。

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


关于随机访问的要点:这可能是推动我认知的原因之一。
我不会说文件必须舒适地放入内存中,而只是放入地址空间中。所以在 64 位系统上,应该没有理由不映射大文件。操作系统知道如何处理它;它与用于交换的逻辑相同,但在这种情况下不需要磁盘上的额外交换空间。
@MvG:您了解磁盘 i/o 的意义吗?如果文件适合地址空间但不适合内存,并且您具有随机访问权限,那么您可能需要进行磁盘磁头移动和查找或 SSD 页面操作的每个记录访问,这将对性能造成灾难。
磁盘 i/o 方面应该独立于访问方法。如果您确实可以随机访问大于 RAM 的文件,则 mmap 和 seek+read 都严重受磁盘限制。否则两者都将从缓存中受益。与内存大小相比,我不认为文件大小是任何一个方向的有力论据。另一方面,文件大小与地址空间是一个非常有力的论据,特别是对于真正的随机访问。
我最初的答案有并且有这一点:“整个事情很适合记忆,或者文件中有引用位置”。所以第二点解决了你在说什么。
B
Ben Collins

mmap 要快得多。您可以编写一个简单的基准测试来向自己证明:

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()-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^(wordsize)。在 32 位机器上,4GB 不够大。期望在 Git 中有 .pack 个文件变得比这大得多并不是不合理的,所以我需要分块读取文件而不求助于常规文件 i/o。在 Boost::Iostreams 的掩护下,我实现了一个 Source,它或多或少是 std::streambufstd::istream 之间交互的另一种视图。您也可以尝试类似的方法,只需将 std::filebuf 继承到 mapped_filebuf 中,类似地,将 std::fstream 继承到 a mapped_fstream 中。两者之间的互动很难做到正确。 Boost::Iostreams 为您完成了一些工作,它还为过滤器和链提供了挂钩,所以我认为以这种方式实现它会更有用。


RE:Windows 上的映射文件缓存。确切地说:启用文件缓冲时,内核内存会在内部映射您正在读取的文件,读入该缓冲区并将其复制回您的进程。就好像您自己记忆映射一样,除非有一个额外的复制步骤。
我不愿意不同意接受的答案,但我相信这个答案是错误的。我按照您的建议在 64 位 Linux 机器上尝试了您的代码,并且 mmap() 并不比 STL 实现快。另外,理论上我不希望'mmap()'更快(或更慢)。
@Tim Cooper:您可能会发现此线程 (markmail.org/message/…) 很有趣。请注意两件事:mmap 在 Linux 中没有得到适当的优化,并且还需要在测试中使用 madvise 以获得最佳结果。
亲爱的本:我读过那个链接。如果 'mmap()' 在 Linux 上不快,而 MapViewOfFile() 在 Windows 上不快,那么你能说“mmap 更快”吗?另外,出于理论上的原因,我认为 mmap() 对于顺序读取来说并不快 - 你有什么相反的解释吗?
本,为什么要费心mmap()一次一页地文件?如果 size_t 的容量足以容纳文件的大小(很可能在 64 位系统上),那么只需 mmap() 一次调用即可处理整个文件。
m
mlbrock

很抱歉 Ben Collins 丢失了他的滑动窗口 mmap 源代码。在 Boost 中拥有那将是很好的。

是的,映射文件要快得多。您实际上是在使用操作系统虚拟内存子系统将内存与磁盘关联起来,反之亦然。这样想:如果操作系统内核开发人员可以让它更快,他们会的。因为这样做几乎可以让一切变得更快:数据库、启动时间、程序加载时间等等。

滑动窗口方法实际上并不难,因为可以一次映射多个连续页面。因此,只要任何一条记录中最大的一条可以放入内存,记录的大小就无关紧要了。重要的是管理簿记。

如果记录不是从 getpagesize() 边界开始,则您的映射必须从前一页开始。映射区域的长度从记录的第一个字节(必要时向下舍入到 getpagesize() 的最接近的倍数)延伸到记录的最后一个字节(向上舍入到 getpagesize() 的最接近的倍数)。处理完一条记录后,您可以 unmap() 它,然后继续下一条。

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


我刚刚用谷歌搜索并找到了这个关于 dwAllocationGranularity 的小片段——我正在使用 dwPageSize 并且一切都被打破了。谢谢!
L
Leon Timmermans

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

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


D
Douglas Leeder

也许您应该预处理文件,因此每条记录都在一个单独的文件中(或者至少每个文件都是可映射的大小)。

您还可以为每条记录完成所有处理步骤,然后再进行下一条记录吗?也许这样可以避免一些 IO 开销?


p
paxos1977

我同意 mmap 的文件 I/O 会更快,但是在您对代码进行基准测试时,反例不应该进行一些优化吗?

本柯林斯写道:

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

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

我建议也尝试:

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 仍然赢了,但这应该让事情更接近。


佚名

我记得几年前将一个包含树结构的巨大文件映射到内存中。与涉及大量内存工作的正常反序列化相比,我对速度感到惊讶,例如分配树节点和设置指针。所以事实上,我正在比较对 mmap (或其在 Windows 上的对应项)的单个调用与对 operator new 和构造函数调用的许多(许多)调用。对于此类任务,与反序列化相比,mmap 是无与伦比的。当然,应该为此研究提升可重定位指针。


这听起来更像是灾难的秘诀。如果对象布局发生变化,你会怎么做?如果你有虚函数,所有的 vftbl 指针都可能是错误的。您如何控制文件映射到的位置?您可以给它一个地址,但这只是一个提示,内核可能会选择另一个基地址。
当您拥有稳定且明确定义的树布局时,这将非常有效。然后,您可以将所有内容转换为相关结构,并通过每次添加“mmap 起始地址”的偏移量来跟踪内部文件指针。这与使用 inode 和目录树的文件系统非常相似
P
Pat Notz

这听起来像是多线程的一个很好的用例......我认为你可以很容易地设置一个线程来读取数据,而其他线程处理它。这可能是一种显着提高感知性能的方法。只是一个想法。


是的。我一直在考虑这个问题,可能会在以后的版本中尝试一下。我唯一的保留是处理比 I/O 延迟要短得多,所以可能没有太多好处。
m
mike

在我看来,使用 mmap() “只是”减轻了开发人员编写自己的缓存代码的负担。在一个简单的“每次读取文件”的情况下,这并不难(尽管 mlbrock 指出您仍然将内存副本保存到进程空间中),但是如果您在文件中来回切换或跳过位等等,我相信内核开发人员在实现缓存方面可能做得比我做得更好......


很可能您可以比内核更好地缓存应用程序特定的数据,内核以非常盲目的方式对页面大小的块进行操作(例如,它仅使用简单的伪 LRU 方案来决定要驱逐哪些页面) - 虽然您可能对正确的缓存粒度了解很多,并且对未来的访问模式也有很好的了解。 mmap 用于缓存的真正好处是您只需重新使用已经存在的现有页面缓存,因此您可以免费获得该内存,并且可以跨进程共享也。
o
ony

我认为 mmap 最大的优点是具有异步读取的潜力:

    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);

问题是我找不到正确的 MAP_FLAGS 来提示应该尽快从文件同步此内存。我希望 MAP_POPULATE 为 mmap 提供正确的提示(即它不会在调用返回之前尝试加载所有内容,但会在异步中使用 feed_data 进行加载)。至少它使用此标志提供了更好的结果,即使手册声明自 2.6.23 以来没有 MAP_PRIVATE 它什么也不做。


您希望 posix_madvise with the WILLNEED 标志用于预填充惰性提示。
@ShadowRanger,听起来很合理。虽然我会更新手册页以明确说明 posix_madvise 是异步调用。对于那些想要等到整个内存区域可用而没有页面错误的人来说,参考 mlock 也很不错。