mmap()与阅读块

问题描述 投票:159回答:12

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

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

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

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

c++ file-io fstream mmap
12个回答
181
投票

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

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

然而,

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

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

  • 一些Java程序员惊讶地发现非阻塞I / O通常比阻塞I / O慢,如果您知道非阻塞I / O需要进行更多的系统调用,这就非常有意义。
  • 其他一些网络程序员震惊地得知poll通常比epoll慢,如果你知道管理MAP_SHARED需要进行更多的系统调用,这是完全合理的。

结论:如果您随机访问数据,保留它很长时间,或者如果您知道可以与其他进程共享,则使用内存映射(如果没有实际共享,则char data[0x1000]; std::ifstream in("file.bin"); while (in) { in.read(data, 0x1000); // do something with data } 不是很有趣)。如果您按顺序访问数据或在读取后丢弃数据,则正常读取文件。如果任何一种方法使您的程序不那么复杂,那么就这样做。对于许多现实世界的情况,没有确定的方法来显示一个更快,而不测试您的实际应用程序而不是基准测试。

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


2
投票

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


1
投票

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


1
投票

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

qazxswpoi

问题是我找不到合适的MAP_FLAGS来提示这个内存应该尽快从文件中同步。我希望MAP_POPULATE为mmap提供正确的提示(即它不会尝试在从调用返回之前加载所有内容,但会在异步中使用feed_data执行此操作)。至少它会使用此标志提供更好的结果,即使该手册指出它自2.6.23起没有MAP_PRIVATE也不执行任何操作。


42
投票

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

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

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

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

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

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


41
投票

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

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的倍数,如何确定何时到达文件的末尾),但它确实不应该比这个。

如果可以,您可以尝试将数据分解为多个文件,这些文件可以是mmap() - 整体而不是部分(更简单)。

几个月前,我对boost_iostreams的滑动窗口mmap() - ed流类进行了半生不熟的实现,但没有人关心,我忙于其他的东西。最不幸的是,几个星期前我删除了一个旧的未完成项目的档案,这是受害者之一:-(

更新:我还应该添加一个警告,即这个基准测试在Windows中看起来会有很大的不同,因为Microsoft实现了一个漂亮的文件缓存,它首先完成了对mmap的大部分操作。即,对于频繁访问的文件,你可以只执行std :: ifstream.read()并且它将与mmap一样快,因为文件缓存已经为你做了内存映射,并且它是透明的。

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

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

滑动窗口mmap听起来很有趣。你能再谈一点吗?

当然 - 我正在为Git编写一个C ++库(一个libgit ++,如果你愿意的话),我遇到了类似的问题:我需要能够打开大(非常大)的文件,而不是性能是一个完整的狗(就像Boost::Iostreams一样)。

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


28
投票

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

mmap看起来很神奇

假设文件已经完全缓存1作为baseline2,mmap可能看起来非常像魔术:

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

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

好吧,它可以。

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

mmap still does per-page work

read(2)mmap(实际上是用于读取块的OS级别系统调用)的主要隐藏成本是,使用mmap,您需要为用户空间中的每个4K页面执行“一些工作”,即使它可能是由页面错误机制隐藏。

举个例子,一个典型的实现只需要minor faultss整个文件就需要进行故障排除所以100 GB / 4K = 2500万个故障来读取100 GB文件。现在,这些将是MAP_POPULATE,但250亿页面错误仍然不会超级快。在最好的情况下,轻微故障的成本可能在100s纳米。

mmap relies heavily on TLB performance

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

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

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

read()避免了这些陷阱

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

  • 每个n字节的malloc调用必须从内核复制N个字节到用户空间。

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

因此,基本上您可以进行以下比较,以确定单个读取大文件的速度更快:

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

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

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

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

...当read()方法变得相对较快时:

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

上述硬件因素在不同平台上变化很大,甚至在同一系列中(例如,在x86代以内,特别是市场领域内)并且明确地跨越体系结构(例如,ARM vs x86与PPC)。

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

  • 添加故障,如上所述,这真的有助于没有MAP_POPULATEcopy_to_user案件。
  • arch/x86/lib/copy_user_64.S中添加快速路径REP MOVQ方法,例如,当它快速时使用read(),这确实有助于page-table isolation案例。

幽灵和熔化后更新

Spectre和Meltdown漏洞的缓解大大增加了系统调用的成本。在我测量的系统上,“无所事事”系统调用的成本(除了调用所做的任何实际工作之外,它是对系统调用的纯开销的估计)从典型的大约100 ns开始现代Linux系统大约700 ns。此外,根据您的系统,由于需要重新加载TLB条目,除了直接系统调用成本之外,专门针对Meltdown的read()修复可能会产生额外的下游影响。

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

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


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

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

3你可以解决这个问题,例如,通过在较小尺寸的窗口中依次进行MAP_POPULATEing,例如100 MB。

4实际上,事实证明faultaround方法(至少有一些硬件/操作系统组合)仅比不使用它快一点,可能是因为内核使用的是char data[0x1000]; std::ifstream in("file.bin"); while (in) { in.read(data, 0x1000); // do something with data } - 因此实际的次要故障数量减少了一倍16左右。


7
投票

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

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

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

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

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


4
投票

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

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


3
投票

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

您还可以在进入下一个记录之前为每个记录执行所有处理步骤吗?也许这会避免一些IO开销?


3
投票

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

本柯林斯写道:

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

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

我建议也尝试:

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

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


2
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.