mmap()与阅读块

时间:2008-09-05 14:52:15

标签: c++ file-io fstream mmap

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

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

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

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

12 个答案:

答案 0 :(得分:177)

我试图在Linux上找到关于mmap / read性能的最后一句话,我在Linux内核邮件列表上遇到了一个很好的帖子(link)。它是从2000开始的,所以从那时起内核中的IO和虚拟内存有了很多改进,但很好地解释了mmapread可能更快或更慢的原因。

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

然而,

  • 对于随机访问,内存映射通常更快,特别是如果您的访问模式稀疏且不可预测。
  • 内存映射允许您保持使用缓存中的页面,直到完成为止。这意味着如果您长时间大量使用文件,然后关闭它并重新打开它,页面仍将被缓存。使用read,您的文件可能早已从缓存中刷新。如果您使用文件并立即丢弃它,则不适用。 (如果您尝试mlock页面只是为了将它们保留在缓存中,那么您正试图超越磁盘缓存,这种类型的漏洞很少有助于系统性能。)
  • 直接阅读文件非常简单快捷。

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

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

  • 其他一些网络程序员感到震惊的是,epoll通常比poll慢,如果您知道管理epoll需要进行更多的系统调用,这就非常有意义。< / p>

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

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

答案 1 :(得分:42)

主要的性能成本是磁盘i / o。 “mmap()”肯定比istream快,但差异可能不明显,因为磁盘i​​ / o将控制你的运行时间。

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

除非你的“记录”很大,否则我肯定分别推荐mmap'每个记录 - 这将非常慢,每个记录需要2次系统调用,可能会丢失页面磁盘内存缓存.....

在你的情况下,我认为mmap(),istream和低级open()/ read()调用都将大致相同。在这些情况下我会推荐mmap():

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

答案 2 :(得分:41)

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一样快,因为文件缓存已经为你完成了内存映射,并且它是透明的。

最终更新:看,人们:在操作系统和标准库以及磁盘和内存层次结构的许多不同平台组合中,我无法确定系统调用{{1}被视为黑匣子,总是总是比mmap快得多。即使我的话可以这样解释,这也不是我的意图。 最后,我的观点是内存映射的i / o通常比基于字节的i / o更快;这仍然是真的。如果你通过实验发现两者之间没有区别,那么对我来说唯一合理的解释就是你的平台在一种有利于调用read的性能的方式下实现了内存映射。绝对确定您以便携方式使用内存映射i / o的唯一方法是使用read。如果您不关心可移植性并且可以依赖目标平台的特定特性,那么使用mmap可能是合适的,而不会牺牲任何性能。

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

  

滑动窗口mmap听起来   有趣。你能说多一点吗?   关于它?

当然 - 我正在为Git编写一个C ++库(一个libgit ++,如果你愿意的话),我遇到了类似的问题:我需要能够打开大(非常大)的文件而没有性能总狗(与read一样)。

std::fstream已经有了一个mapped_file源,但问题是它是Boost::Iostreams ping整个文件,这限制你2 ^(单词大小)。在32位机器上,4GB不够大。期望Git中的mmap文件变得比这大得多,这是不合理的,所以我需要以块的形式读取文件,而不需要使用常规文件i / o。在.pack的封面下,我实施了一个来源,这或多或少是Boost::Iostreamsstd::streambuf之间互动的另一种观点。您也可以尝试使用类似方法,只需将std::istream继承到std::filebuf,同样将mapped_filebuf继承到std::fstream。这是两者之间的相互作用,很难做到正确。 a mapped_fstream已经完成了一些工作,它还提供了过滤器和链的钩子,所以我认为以这种方式实现它会更有用。

答案 3 :(得分:28)

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

mmap看起来像魔术

假设文件已经完全缓存 1 作为基线 2 mmap可能看起来非常像 magic

  1. mmap只需要1个系统调用(可能)映射整个文件,之后不再需要系统调用。
  2. mmap并不需要从内核到用户空间的文件数据副本。
  3. mmap允许您访问文件&#34;作为内存&#34;,包括使用您可以针对内存执行的任何高级技巧处理它,例如编译器自动矢量化,SIMD内在函数,预取,优化的内存中解析例程,OpenMP等
  4. 如果文件已经在缓存中,似乎无法击败:您只需直接访问内核页面缓存作为内存,它就不会比这更快。

    嗯,它可以。

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

    mmap仍然按页面工作

    mmapread(2)的主要隐藏费用(实际上是读取块的操作系统级别系统调用)是mmap你&# 39;我需要做一些工作&#34;对于用户空间中的每个4K页面,即使它可能被页面错误机制隐藏。

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

    mmap在很大程度上依赖于TLB性能

    现在,您可以将MAP_POPULATE传递给mmap,告诉它在返回之前设置所有页面表,因此访问时不应出现页面错误。现在,它有一个小问题,它也将整个文件读入RAM,如果你试图映射一个100GB的文件会爆炸 - 但是现在让我们忽略它 3 。内核需要执行每页工作来设置这些页表(显示为内核时间)。这最终成为mmap方法的主要成本,并且它与文件大小成正比(即,随着文件大小的增长,它不会变得相对不那么重要) 4

    最后,即使在用户空间访问这样的映射也不是完全免费的(与不是来自基于文件的mmap的大型内存缓冲区相比) - 即使设置了页表,从概念上讲,每次访问新页面都会导致TLB未命中。由于mmap文件意味着使用页面缓存及其4K页面,因此对于100GB文件,再次花费2500万次。

    现在,这些TLB未命中的实际成本在很大程度上取决于硬件的以下几个方面:(a)您拥有多少4K TLB内容以及其余的转换缓存如何执行(b)硬件的好坏prefetch处理TLB - 例如,prefetch触发页面遍历? (c)页面行走硬件的速度和平行程度。在现代高端x86英特尔处理器上,页面行走硬件通常非常强大:至少有2个并行页面步行器,页面遍历可以同时发生并继续执行,硬件预取可以触发页面遍历。因此,TLB对读取负载的影响相当低 - 无论页面大小如何,此类负载通常都会执行相似的操作。但是,其他硬件通常要差得多!

    read()避免了这些陷阱

    read()系统调用,通常是&#34;块读取&#34;例如,在C,C ++和其他语言中提供的类型调用具有一个主要缺点,即每个人都清楚地意识到:

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

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

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

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

    在许多系统中,它们实际上是近似平衡的。请注意,每个都使用完全不同的硬件和操作系统堆栈属性进行扩展。

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

    • 操作系统具有快速的小故障处理功能,尤其是故障排除优化,例如故障排除。
    • 操作系统具有良好的MAP_POPULATE实现,可以有效处理大型地图,例如,底层页面在物理内存中是连续的。
    • 硬件具有强大的页面翻译性能,如大型TLB,快速二级TLB,快速并行页面漫步,良好的预翻译与翻译等。

    ... read()方法在以下时间变得相对较快:

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

    上述硬件因素在不同平台上变得非常 ,即使在同一系列中(例如,在x86代内,特别是在市场领域内),并且肯定跨越架构(例如,ARM vs x86与PPC)。

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

    • 如上所述添加了故障,这对mmap案件没有MAP_POPULATE非常有帮助。
    • copy_to_user中添加快速路径arch/x86/lib/copy_user_64.S方法,例如在快速时使用REP MOVQ,这对read()案例有帮助。
    幽灵和熔化后的

    更新

    Spectre和Meltdown漏洞的缓解大大增加了系统调用的成本。在我测量过的系统上,“不做任何事情”的成本是什么&#34;系统调用(除了调用所做的任何实际工作之外,它是对系统调用的纯开销的估计)从典型的现代Linux系统上的大约100 ns变为大约700 ns。此外,根据您的系统,由于需要重新加载TLB条目,除了直接系统调用成本之外,专门针对Meltdown的page-table isolation修补程序还可能产生额外的下游影响。

    与基于read()的方法相比,所有这些都是基于mmap的方法的相对劣势,因为read()方法必须为每个&#34;缓冲区大小&#进行一次系统调用34;值得的数据。您无法随意增加缓冲区大小以分摊此成本,因为使用大型缓冲区通常会因为超过L1大小而导致性能下降,因此不断遭遇缓存未命中。

    另一方面,使用mmap,您可以使用MAP_POPULATE在大内存区域中进行映射并有效地访问它,但代价是只有一个系统调用。

    1 这或多或少还包括文件未完全缓存的情况,但操作系统预读足够好以使其显示如此(即,页面通常在您需要的时候进行缓存)。这是一个微妙的问题,因为预读的方式通常在mmapread来电之间有所不同,并且可以通过&#34;建议&#34;进一步调整。如 2 中所述进行调用。

    2 ...因为如果文件缓存,那么您的行为将完全由IO问题所主导,包括您的访问模式对于底层硬件 - 你所有的努力应该是确保这种访问尽可能具有同情心,例如通过使用madvisefadvise调用(以及您可以进行的任何应用程序级别更改来改进访问模式)。

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

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

答案 4 :(得分:7)

对不起Ben Collins丢失了他的滑动窗口mmap源代码。在Boost中很高兴。

是的,映射文件要快得多。您实际上是使用操作系统虚拟内存子系统来关联内存到磁盘,反之亦然。以这种方式思考:如果操作系统内核开发人员可以更快地实现它。因为这样做可以更快地完成所有事情:数据库,启动时间,程序加载时间等等。

滑动窗口方法确实不是那么困难,因为可以同时映射多个连续页面。因此,只要任何单个记录中的最大记录适合内存,记录的大小就无关紧要。重要的是管理簿记。

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

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

答案 5 :(得分:4)

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

话虽如此,可能有更好的途径来改善表现。您说输入文件被多次扫描,如果您可以一次性读取它然后完成它,那可能会更快。

答案 6 :(得分:3)

我同意mmap的文件I / O会更快,但是在对代码进行基准测试时,计数器示例是否应该稍微优化?

Ben Collins写道:

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'd文件我/ O仍然获胜,但这应该让事情更加接近。

答案 7 :(得分:2)

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

您还可以为每个记录执行所有处理步骤,然后再转到下一个记录吗?也许这会避免一些IO开销?

答案 8 :(得分:2)

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

答案 9 :(得分:2)

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

答案 10 :(得分:1)

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

答案 11 :(得分: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也不执行任何操作。