关于c ++:mmap()与阅读块

关于c ++:mmap()与阅读块

mmap() vs. reading blocks

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

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

mmap()代码可能会变得非常混乱,因为mmap d块需要位于页面大小的边界上(据我的理解),记录可能会跨越页面边界。使用fstream s时,由于我们不限于读取位于页面大小边界上的块,因此我只能寻求记录的开头并再次开始读取。

我如何在这两个选项之间做出决定,而无需先实际编写完整的实现?任何经验法则(例如mmap()快2倍)还是简单测试?


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

  • 调用mmap的开销比read的开销大(就像epoll的开销比poll的开销更大,而poll的开销比read更大)。出于某些原因,更改虚拟内存映射在某些处理器上是一项非常昂贵的操作,原因是在不同进程之间进行切换很昂贵。
  • IO系统已经可以使用磁盘高速缓存,因此,无论您使用哪种方法,如果读取文件,都会访问高速缓存或错过高速缓存。

然而,

  • 对于随机访问,内存映射通常更快,尤其是在您的访问模式稀疏且不可预测的情况下。
  • 内存映射使您可以继续使用缓存中的页面,直到完成操作为止。这意味着,如果长时间使用大量文件,然后将其关闭并重新打开,页面仍将被缓存。使用read,您的文件可能早已从高速缓存中清除了。如果您使用文件并立即丢弃它,则此方法不适用。 (如果您尝试mlock页只是为了将其保留在缓存中,则您试图使磁盘缓存的性能超过智能,这种愚蠢的做法很少会提高系统性能)。
  • 直接读取文件非常简单快捷。

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

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

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

结论:如果您随机访问数据,将其保留很长时间,或者您知道可以与其他进程共享(如果没有实际共享,则MAP_SHARED并不是很有趣),请使用内存映射。如果您顺序访问数据或在读取后将其丢弃,则通常读取文件。并且,如果这两种方法都使您的程序不那么复杂,请这样做。在许多现实情况下,如果不测试您的实际应用程序而不是基准,就无法确定显示更快的方法。

(对这个问题的回答很抱歉,但我一直在寻找答案,并且这个问题一直出现在Google搜索结果的顶部。)


主要的性能成本将是磁盘I / O。" mmap()"当然比istream快,但是这种差异可能并不明显,因为磁盘I / O将主导您的运行时。

我尝试了Ben Collins的代码片段(请参见上/下),以测试他对" mmap()更快"的断言,但没有发现可测量的差异。看到我对他的回答的评论。

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

在您的情况下,我认为mmap(),istream和低级open()/ read()调用几乎都是相同的。在以下情况下,我建议使用mmap():

  • 文件中存在随机访问(非顺序访问),并且
  • 整个文件都适合放在内存中,或者文件内有引用位置,因此可以将某些页面映射到其他页面中。这样,操作系统使用可用的RAM可获得最大收益。
  • 或者,如果多个进程正在读取/在同一个文件上工作,则mmap()很棒,因为这些进程都共享相同的物理页面。
  • (顺便说一句-我爱mmap()/ MapViewOfFile())。


    mmap更快。您可以编写一个简单的基准来向自己证明:

    1
    2
    3
    4
    5
    6
    7
    8
    char data[0x1000];
    std::ifstream in("file.bin");

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

    与:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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一样快,因为文件缓存已经为您完成了内存映射,并且是透明的。

    最终更新:您好,人们:在OS和标准库以及磁盘和内存层次结构的许多不同平台组合中,我不能肯定地说系统调用mmap(被视为黑匣子)将始终始终比read快得多。即使我的话可以这样解释,也不完全是我的意图。最终,我的观点是,内存映射的I / O通常比基于字节的I / O更快。这仍然是事实。如果您实验性地发现两者之间没有区别,那么对我来说唯一合理的解释是您的平台在幕后实施了内存映射,从而有利于执行对read的调用。绝对确定您以可移植方式使用内存映射I / O的唯一方法是使用mmap。如果您不关心可移植性,并且可以依赖于目标平台的特定特性,那么使用read可能是合适的,而不会牺牲任何性能。

    编辑以清理答案列表:
    @jbl:

    the sliding window mmap sounds
    interesting. Can you say a little more
    about it?

    当然-我当时正在为Git(如果愿意的话,为libgit ++)编写一个C ++库,并且遇到了与此类似的问题:我需要能够打开大型文件(大型文件),而性能却不高(与std::fstream一样)。

    Boost::Iostreams已经具有一个mapping_file源,但是问题在于它mmap ping整个文件,这将您限制为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已经为您完成了一些工作,并且还为过滤器和链提供了挂钩,所以我认为以这种方式实现它会更有用。


    这里已经有很多很好的答案,涵盖了很多要点,所以我只添加一些我没有直接在上面解决的问题。也就是说,此答案不应被视为全面的利弊,而应视为此处其他答案的附录。

    好。

    mmap看起来像魔术

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

    好。

  • mmap仅需要1个系统调用即可(可能)映射整个文件,此后不再需要系统调用。
  • mmap不需要将文件数据从内核复制到用户空间。
  • mmap允许您"作为内存"访问文件,包括使用可以对内存执行的任何高级技巧处理文件,例如编译器自动向量化,SIMD内部函数,预取,优化的内存解析例程,OpenMP等。
  • 如果文件已经在高速缓存中,则似乎无法克服:您只是直接访问内核页面高速缓存作为内存,并且它的速度不能超过此速度。

    好。

    好吧,可以。

    好。

    mmap实际上不是魔术,因为...

    mmap仍然可以按页面工作

    mmapread(2)(这实际上是读取块的可比操作系统级系统调用)的主要隐藏成本是,使用mmap,您需要为用户空间中的每个4K页面做"一些工作" ,即使它可能被页面错误机制隐藏了。

    好。

    例如,一个典型的实现只是整个文件的mmap就需要进行错误修复,因此100 GB / 4K = 2500万个错误才能读取100 GB的文件。现在,这些将是次要的错误,但是250亿页的错误仍然不会很快。在最佳情况下,一次小故障的成本可能约为100纳米。

    好。

    mmap严重依赖TLB性能

    现在,您可以将MAP_POPULATE传递给mmap,以告诉它在返回之前设置所有页表,因此访问它时应该没有页面错误。现在,这有一个小问题,它也将整个文件读入RAM,如果您尝试映射100GB的文件,该文件将被炸毁-但现在我们就忽略它。内核需要做每页工作以设置这些页表(显示为内核时间)。这最终成为mmap方法的主要成本,并且与文件大小成正比(即,随着文件大小的增加,它的重要性也不会相对降低)4。

    好。

    最后,即使在用户空间中访问,这种映射也不是完全免费的(与不是源自基于文件的mmap的大内存缓冲区相比)-即使设置了页表,对新页的每次访问也是从概念上讲,这将导致TLB错过。因为mmap加密文件意味着使用页面缓存及其4K页面,所以对于100GB的文件,这又需要花费2500万次。

    好。

    现在,这些TLB缺失的实际成本在很大程度上至少取决于硬件的以下方面:(a)您拥有多少个4K TLB实体以及其余的转换缓存如何工作(b)硬件预取处理得如何好使用TLB-例如,预取能否触发页面浏览? (c)分页浏览硬件的速度和并行度。在现代高端x86 Intel处理器上,页面浏览硬件通常非常强大:至少有2个并行页面浏览器,页面浏览可以与连续执行同时发生,并且硬件预取可以触发页面浏览。因此,TLB对流式读取负载的影响非常小-而且无论页面大小如何,这种负载通常都将以类似的方式执行。但是,其他硬件通常更差!

    好。

    read()避免了这些陷阱

    read()系统调用通常是"块读取"类型调用(例如,以C,C ++和其他语言提供)的基础,它的一个主要缺点是每个人都应该清楚:

    好。

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

    另一方面,它可以避免上述大部分费用-您无需将2500万个4K页面映射到用户空间。通常,您可以在用户空间中malloc单个缓冲区中的小缓冲区,然后在所有read调用中重复使用该缓冲区。在内核方面,几乎没有4K页或TLB遗漏的问题,因为通常使用几个非常大的页(例如,x86上的1 GB页)线性映射所有RAM,因此覆盖了页缓存中的基础页在内核空间中非常有效。

    好。

    因此,基本上,您可以通过以下比较来确定对大文件的单次读取速度更快:

    好。

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

    好。

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

    好。

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

    好。

  • 该操作系统具有快速的轻微故障处理功能,尤其是诸如故障排除之类的轻微故障批量优化。
  • 该OS具有良好的MAP_POPULATE实现,可以在例如基础页面在物理内存中连续的情况下有效处理大型地图。
  • 硬件具有强大的页面翻译性能,例如大型TLB,快速的第二级TLB,快速和并行的页面遍历器,与翻译的良好预取交互等。
  • 好。

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

    好。

  • read()系统调用具有良好的复制性能。例如,内核方面良好的copy_to_user性能。
  • 内核具有一种有效的(相对于用户态)映射内存的方式,例如仅使用少数几个具有硬件支持的大页面。
  • 内核具有快速的系统调用,并且可以在整个系统调用之间保留内核TLB条目。
  • 好。

    上述硬件因素在不同平台之间(甚至在同一系列内(例如在x86代之内,尤其是细分市场中))差异很大,并且在不同体系结构(例如ARM,x86和PPC)之间也存在很大差异。

    好。

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

    好。

  • 如上所述,增加了故障排除功能,这确实有助于没有MAP_POPULATEmmap情况。
  • arch/x86/lib/copy_user_64.S中添加快速路径copy_to_user方法,例如,在快速时使用REP MOVQ,这确实有助于read()的情况。
  • 好。

    幽灵和崩溃后更新

    Spectre和Meltdown漏洞的缓解措施大大增加了系统调用的成本。在我测量的系统上,"不执行任何操作"系统调用(除了该调用完成的任何实际工作之外,它是系统调用的纯开销的估计)的成本大约为100 ns现代Linux系统大约需要700 ns。此外,根据您的系统,由于需要重新加载TLB条目,专门用于Meltdown的页表隔离修复程序可能会具有其他下游影响,除了直接的系统调用成本之外。

    好。

    与基于mmap的方法相比,所有这些都是基于read()的方法的相对缺点,因为read()方法必须针对每个"缓冲区大小"的数据进行一次系统调用。您不能任意增加缓冲区大小以分摊此成本,因为使用大型缓冲区通常会变得更糟,因为您超过了L1的大小,因此不断遭受高速缓存未命中的困扰。

    好。

    另一方面,使用mmap,您可以使用MAP_POPULATE在较大的内存区域中进行映射,并可以有效地对其进行访问,而仅需进行一次系统调用即可。

    好。

    1这种或多或少的情况还包括文件没有被完全缓存到开始的情况,但预读操作系统足以使文件看起来像这样(例如,页面通常在您存储时被缓存)。想要它)。但是,这是一个微妙的问题,因为mmapread调用之间的预读方式通常很不相同,并且可以通过"建议"调用进一步调整,如2中所述。

    好。

    2 ...因为如果不缓存文件,那么您的行为将完全由IO问题决定,包括您对底层硬件的访问模式有多同情-您应尽一切努力确保此类访问具有同情心 可能的,例如 通过使用madvisefadvise调用(以及可以对应用程序级别进行的任何更改来改善访问模式)。

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

    4实际上,事实证明MAP_POPULATE方法(至少是一些硬件/操作系统组合)仅比不使用它快一点,这可能是因为内核正在使用故障排除方法-因此,实际的次要故障数减少了 系数为16左右。

    好。


    抱歉,本·科林斯(Ben Collins)丢失了滑动窗口的mmap源代码。在Boost中拥有它真是太好了。

    是的,映射文件要快得多。本质上,您是在使用OS虚拟内存子系统来将内存与磁盘关联,反之亦然。这样考虑:如果OS内核开发人员可以使其更快,他们就会这样做。因为这样做可以使几乎所有事情变得更快:数据库,启动时间,程序加载时间等等。

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

    如果记录不是从getpagesize()边界开始的,则映射必须从前一页开始。映射区域的长度从记录的第一个字节(必要时向下舍入到getpagesize()的最接近倍数)到记录的最后一个字节(舍入到getpagesize()的最接近倍数)。处理完记录后,可以取消对其的映射(),然后移至下一条。

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


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

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


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

    本·科林斯写道:

    1
    2
    3
    4
    5
    6
    7
    8
    char data[0x1000];
    std::ifstream in("file.bin");

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

    我建议也尝试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    char data[0x1000];
    std::ifstream iifle("file.bin");
    std::istream  in( ifile.rdbuf() );

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

    除此之外,您还可以尝试使缓冲区大小与虚拟内存的一页大小相同,以防万一0x1000不是您计算机上虚拟内存的一页大小...恕我直言,仍然有文件I / O胜,但这应该使事情变得更紧密。


    也许您应该对文件进行预处理,所以每个记录都在一个单独的文件中(或者至少每个文件都可以映射)。

    还可以在移至下一条记录之前对每条记录执行所有处理步骤吗?也许这样可以避免一些IO开销?


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


    在我看来,使用mmap()"只是"使开发人员不必编写自己的缓存代码。在一个简单的"一次读取文件一次"的情况下,这并不困难(尽管mlbrock指出您仍将内存副本保存到进程空间中),但是如果要在文件中来回移动或跳过诸如此类,我相信内核开发人员在实现缓存方面可能做得比我更好。


    我认为mmap的最大优点是可以异步读取:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        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来提示应该从文件asap同步此内存。
    我希望MAP_POPULATE为mmap提供正确的提示(即,它不会在调用返回之前尝试加载所有内容,但会通过feed_data异步进行加载)。至少它使用此标志提供了更好的结果,即使手册指出自2.6.23起没有MAP_PRIVATE也不执行任何操作。


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


    推荐阅读