221

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

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

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

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

4

12 回答 12

244

我试图找到关于 Linux 上 mmap / 读取性能的最终决定,我在 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如果没有实际共享,则不是很有趣),请使用内存映射。如果您按顺序访问数据或在读取后将其丢弃,则可以正常读取文件。如果任何一种方法都可以让你的程序变得不那么复杂,那就这样做吧。对于许多现实世界的案例,如果不测试您的实际应用程序而不是基准测试,就无法确定一种方法会更快。

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

于 2011-06-17T08:33:01.077 回答
66

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

mmap 看起来很神奇

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

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

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

嗯,它可以。

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

mmap 仍然可以按页面工作

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

举个例子,一个典型的实现,只mmap需要整个文件就需要故障输入,所以 100 GB / 4K = 2500 万次故障才能读取 100 GB 文件。现在,这些将是小错误,但 2500 万页错误仍然不会超快。在最好的情况下,一个小故障的成本可能在 100 纳米。

mmap 严重依赖 TLB 性能

现在,您可以通过MAP_POPULATEtommap告诉它在返回之前设置所有页表,因此在访问它时应该没有页面错误。现在,这有一个小问题,它还将整个文件读入 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这或多或少还包括文件未完全缓存的情况,但操作系统的预读足以使其显示为想要它)。这是一个微妙的问题,因为预读的工作方式在mmapandread调用之间通常非常不同,并且可以通过“advise”调用进一步调整,如2中所述。

2 ...因为如果文件没有被缓存,你的行为将完全被 IO 问题所支配,包括你的访问模式对底层硬件的同情程度——你所有的努力都应该确保这种访问与可能的,例如通过使用madviseorfadvise调用(以及您可以进行的任何应用程序级别更改以改进访问模式)。

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

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

于 2017-01-01T21:51:55.470 回答
48

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

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

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

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

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

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

于 2008-09-30T05:03:18.190 回答
42

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

于 2008-09-05T15:12:02.247 回答
7

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

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

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

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

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

于 2008-09-15T23:09:57.787 回答
5

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

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

于 2008-09-05T15:11:22.450 回答
3

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

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

于 2008-09-05T15:37:26.443 回答
3

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

于 2008-09-30T05:31:49.087 回答
3

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

于 2014-03-27T19:18:47.580 回答
2

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

于 2008-09-06T13:41:00.240 回答
2

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

于 2009-03-29T18:02:26.497 回答
1

我认为 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 它什么也不做。

于 2009-12-13T16:59:37.057 回答