关于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也不执行任何操作。


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


    推荐阅读

      linux输入过的命令?

      linux输入过的命令?,系统,地址,数字,命令,工具,工作,环境,界面,历史,指令,lin

      linux命令添加文件?

      linux命令添加文件?,工作,简介,数据,系统,文件,命令,操作,文件名,内容,终端,l

      linux退出启动命令行?

      linux退出启动命令行?,系统,状态,档案,平台,命令,环境,模式,终端,程序,编辑,l

      linux进程运行命令?

      linux进程运行命令?,系统,工作,状态,地址,信息,进程,基础,命令,管理,软件,lin

      linux文件输入命令?

      linux文件输入命令?,工作,系统,地址,信息,工具,位置,命令,设备,发行,首开,lin

      文件备份命令linux?

      文件备份命令linux?,网站,系统,设备,文件,软件,网络,工具,环境,数据,地址,lin

      linux遍历文件命令?

      linux遍历文件命令?,系统,数据,工具,文件,平台,信息,百度,位置,时间,适当,lin

      linux命令查看小文件?

      linux命令查看小文件?,系统,档案,文件夹,标准,软件,单位,文件,命令,大小,内

      linux文件中剪切命令?

      linux文件中剪切命令?,位置,系统,工作,命令,发行,连续,标准,终端,文件,目录,l

      linux命令行不能输入?

      linux命令行不能输入?,工作,系统,电脑,服务,命令,名字,首次,百度,管理,第一,l

      linux存储文件命令?

      linux存储文件命令?,系统,地址,工作,命令,软件,电脑,标准,底部,信息,文件,lin

      linux保存命令文件?

      linux保存命令文件?,系统,状态,命令,文件,第一,管理,电脑,模式,编辑,终端,lin

      linux私有文件命令?

      linux私有文件命令?,系统,工作,工具,命令,设备,文件,目录,位置,不了,情况,Lin

      linux中命令如何输入?

      linux中命令如何输入?,系统,电脑,地址,工具,发行,命令,终端,密码,名字,网站,l

      linux中启动服务命令?

      linux中启动服务命令?,服务,系统,命令,信息,工作,设备,网络,标准,名称,密码,l

      删除linux文件命令?

      删除linux文件命令?,名称,不了,文件夹,命令,文件,目录,方法,指令,子目录,选

      linux文件录入命令?

      linux文件录入命令?,系统,命令,网络,标准,时间,密码,名字,管理,文件,文件夹,L

      文件复制命令linux?

      文件复制命令linux?,系统,地址,文件,目录,位置,工具,命令,目标,文件名,源文

      linux显示运行命令?

      linux显示运行命令?,系统,服务,状态,信息,工具,数据,电脑,标准,管理,时间,如

      linux命令移除文件夹?

      linux命令移除文件夹?,命令,文件夹,通用,不了,数据,名称,档案,系统,文件,目