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

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

mmap()代码可能会变得非常混乱,因为mmap的块需要位于页面大小的边界上(我的理解),而记录可能位于页面边界上。使用fstreams,我可以只寻找记录的开始并重新开始读取,因为我们不局限于读取位于页面大小边界上的块。

如果不首先编写完整的实现,我如何在这两个选项之间做出决定呢?有什么经验法则(例如,mmap()快2倍)或简单的测试吗?


mmap应该更快,但我不知道有多快。这在很大程度上取决于你的代码。如果您使用mmap,最好一次映射整个文件,这会使您的工作更容易。一个潜在的问题是,如果您的文件大于4GB(或者实际上限制更低,通常是2GB),您将需要一个64位架构。所以如果你在使用32的环境,你可能不想使用它。

话虽如此,但或许有更好的方法来提高业绩。你说过输入文件会被扫描很多次,如果你可以一次读取它,然后处理它,这可能会更快。


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中看起来会有很大不同,因为微软实现了一个漂亮的文件缓存,它首先完成了您在mmap中所做的大部分工作。例如,对于经常访问的文件,你可以执行std::ifstream.read(),它会和mmap一样快,因为文件缓存已经为你做了一个内存映射,而且它是透明的。

Final Update: Look, people: across a lot of different platform combinations of OS and standard libraries and disks and memory hierarchies, I can't say for certain that the system call mmap, viewed as a black box, will always always always be substantially faster than read. That wasn't exactly my intent, even if my words could be construed that way. Ultimately, my point was that memory-mapped i/o is generally faster than byte-based i/o; this is still true. If you find experimentally that there's no difference between the two, then the only explanation that seems reasonable to me is that your platform implements memory-mapping under the covers in a way that is advantageous to the performance of calls to read. The only way to be absolutely certain that you're using memory-mapped i/o in a portable way is to use mmap. If you don't care about portability and you can rely on the particular characteristics of your target platforms, then using read may be suitable without sacrificing measurably any performance.

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

滑动窗口mmap发出声音 有趣。你能多说一点吗 呢?

当然-我正在为Git写一个c++库(一个libgit++,如果你愿意的话),我遇到了一个类似的问题:我需要能够打开大(非常大)的文件,而不是有一个完全的性能狗(因为它将与std::fstream)。

Boost::Iostreams already has a mapped_file Source, but the problem was that it was mmapping whole files, which limits you to 2^(wordsize). On 32-bit machines, 4GB isn't big enough. It's not unreasonable to expect to have .pack files in Git that become much larger than that, so I needed to read the file in chunks without resorting to regular file i/o. Under the covers of Boost::Iostreams, I implemented a Source, which is more or less another view of the interaction between std::streambuf and std::istream. You could also try a similar approach by just inheriting std::filebuf into a mapped_filebuf and similarly, inheriting std::fstream into a mapped_fstream. It's the interaction between the two that's difficult to get right. Boost::Iostreams has some of the work done for you, and it also provides hooks for filters and chains, so I thought it would be more useful to implement it that way.


也许您应该对文件进行预处理,这样每个记录都在一个单独的文件中(或者至少每个文件都是mmap可用的大小)。

另外,您能否在处理下一条记录之前完成每条记录的所有处理步骤?也许这样可以避免一些IO开销?


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


我很抱歉本·柯林斯丢失了他的滑动窗口mmap源代码。这在Boost中是很好的。

是的,映射文件要快得多。您实际上是在使用OS虚拟内存子系统来关联内存和磁盘,反之亦然。可以这样想:如果OS内核开发者可以让它更快,他们会的。因为这样做几乎使所有事情都更快:数据库、启动时间、程序加载时间等等。

滑动窗口方法实际上并不难,因为可以一次映射多个连续的页面。因此,记录的大小并不重要,只要最大的记录可以放入内存。重要的是做好簿记工作。

如果一个记录不是从getpagesize()边界开始,那么映射就必须从前一页开始。映射区域的长度从记录的第一个字节(如有必要向下舍入到getpagesize()的最近倍数)扩展到记录的最后一个字节(四舍五入到getpagesize()的最近倍数)。当您完成一条记录的处理后,您可以unmap()它,然后继续到下一条记录。

这在Windows下工作也很好,使用CreateFileMapping()和MapViewOfFile()(和GetSystemInfo()来获取SYSTEM_INFO。dwAllocationGranularity——不是SYSTEM_INFO.dwPageSize)。


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

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

我当然不建议逐个逐个地mmap每条记录,除非你的“记录”非常大——那样会非常慢,每条记录需要2个系统调用,而且可能会从磁盘内存缓存.....中丢失页面

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

文件中有随机访问(而不是顺序访问)和 或者在文件中存在引用位置,以便某些页面可以映射进来,其他页面可以映射出去。这样操作系统就能最大限度地利用可用RAM。 或者,如果多个进程正在读取/处理同一个文件,那么mmap()非常有用,因为所有进程都共享相同的物理页面。

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


我同意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不是您机器上一页虚拟内存的大小……IMHO mmap文件I/O仍然是赢家,但这应该使事情更接近。


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


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


我试图找到关于Linux上mmap / read性能的最后一句话,我在Linux内核邮件列表上看到了一个不错的帖子(链接)。从2000年开始,内核对IO和虚拟内存进行了很多改进,但这很好地解释了为什么mmap或read可能更快或更慢。

调用mmap的开销比读取的多(就像epoll比poll的开销多,poll的开销比读取的多)。在某些处理器上,更改虚拟内存映射是一项相当昂贵的操作,原因与在不同进程之间切换成本相同。 IO系统已经可以使用磁盘缓存,所以如果读取一个文件,无论使用什么方法,都将命中缓存或错过缓存。

然而,

Memory maps are generally faster for random access, especially if your access patterns are sparse and unpredictable. Memory maps allow you to keep using pages from the cache until you are done. This means that if you use a file heavily for a long period of time, then close it and reopen it, the pages will still be cached. With read, your file may have been flushed from the cache ages ago. This does not apply if you use a file and immediately discard it. (If you try to mlock pages just to keep them in cache, you are trying to outsmart the disk cache and this kind of foolery rarely helps system performance). Reading a file directly is very simple and fast.

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

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

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

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


I remember mapping a huge file containing a tree structure into memory years ago. I was amazed by the speed compared to normal de-serialization which involves lot of work in memory, like allocating tree nodes and setting pointers. So in fact I was comparing a single call to mmap (or its counterpart on Windows) against many (MANY) calls to operator new and constructor calls. For such kind of task, mmap is unbeatable compared to de-serialization. Of course one should look into boosts relocatable pointer for this.


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

Mmap看起来就像魔法

如果文件已经完全cached1作为基线2,mmap可能看起来很像魔术:

Mmap只需要一个系统调用来映射整个文件(可能),之后就不需要更多的系统调用了。 Mmap不需要将文件数据从内核复制到用户空间。 mmap允许您以“内存”的形式访问文件,包括使用您可以针对内存做的任何高级技巧来处理它,例如编译器自动向量化、SIMD intrinsic、预取、优化的内存解析例程、OpenMP等。

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

是的,它可以。

Mmap其实并不神奇,因为……

Mmap仍然执行逐页工作

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

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

mmap严重依赖于TLB性能

Now, you can pass MAP_POPULATE to mmap to tell it to set up all the page tables before returning, so there should be no page faults while accessing it. Now, this has the little problem that it also reads the entire file into RAM, which is going to blow up if you try to map a 100GB file - but let's ignore that for now3. The kernel needs to do per-page work to set up these page tables (shows up as kernel time). This ends up being a major cost in the mmap approach, and it's proportional to the file size (i.e., it doesn't get relatively less important as the file size grows)4.

最后,即使在用户空间中访问这样的映射也不是完全免费的(与不是来自基于文件的mmap的大内存缓冲区相比)——即使一旦设置了页表,从概念上讲,对新页的每次访问都将引起TLB miss。因为mmap文件意味着使用页缓存和它的4K页面,所以对于100GB文件,您又会引起这个成本2500万次。

Now, the actual cost of these TLB misses depends heavily on at least the following aspects of your hardware: (a) how many 4K TLB enties you have and how the rest of the translation caching works performs (b) how well hardware prefetch deals with with the TLB - e.g., can prefetch trigger a page walk? (c) how fast and how parallel the page walking hardware is. On modern high-end x86 Intel processors, the page walking hardware is in general very strong: there are at least 2 parallel page walkers, a page walk can occur concurrently with continued execution, and hardware prefetching can trigger a page walk. So the TLB impact on a streaming read load is fairly low - and such a load will often perform similarly regardless of the page size. Other hardware is usually much worse, however!

Read()避免了这些陷阱

在C、c++和其他语言中,read()系统调用通常是“块读”类型调用的基础,它有一个每个人都很清楚的主要缺点:

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

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

所以基本上你可以通过下面的比较来确定对于一个大文件的单次读取,哪个更快:

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

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

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

该操作系统具有快速的小故障处理,特别是小故障膨胀优化,如故障规避。 该操作系统具有良好的MAP_POPULATE实现,可以有效地处理大型映射,例如,底层页面在物理内存中是连续的。 硬件具有较强的页面转换性能,如大tlb、快速的二级tlb、快速并行的走页器、与翻译良好的预取交互等。

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

read()系统调用具有良好的复制性能。例如,内核端良好的copy_to_user性能。 内核有一种有效的(相对于用户区域)映射内存的方法,例如,在硬件支持下只使用几个大页面。 内核具有快速的系统调用,并且有一种方法可以跨系统调用保持内核TLB条目。

上述硬件因素在不同的平台上有很大的差异,甚至在同一个家族中(例如,在x86代中,特别是在市场细分中),并且肯定在不同的架构中(例如,ARM vs x86 vs PPC)。

操作系统因素也在不断变化,双方的各种改进导致一种方法或另一种方法的相对速度有了很大的飞跃。最近的一份名单包括:

添加了上面描述的绕过错误的功能,这确实有助于在没有MAP_POPULATE的情况下使用mmap。 在arch/x86/lib/copy_user_64中增加了快速copy_to_user方法。S,例如,使用REP MOVQ时,它是快速的,这真的有助于read()情况。

在Spectre和Meltdown之后更新

The mitigations for the Spectre and Meltdown vulnerabilities considerably increased the cost of a system call. On the systems I've measured, the cost of a "do nothing" system call (which is an estimate of the pure overhead of the system call, apart from any actual work done by the call) went from about 100 ns on a typical modern Linux system to about 700 ns. Furthermore, depending on your system, the page-table isolation fix specifically for Meltdown can have additional downstream effects apart from the direct system call cost due to the need to reload TLB entries.

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

另一方面,使用mmap,您可以用MAP_POPULATE映射一个大内存区域,并有效地访问它,只需要一个系统调用。


1这或多或少也包括文件一开始没有完全缓存的情况,但操作系统预读足够好,使它看起来是这样的(即,页面通常在你想要它的时候被缓存)。这是一个微妙的问题,因为预读在mmap和read调用之间的工作方式通常是非常不同的,并且可以通过“advise”调用进一步调整,如2中所述。

2…因为如果文件没有被缓存,你的行为将完全由IO考虑决定,包括你的访问模式与底层硬件的一致程度——你所有的努力都应该确保这样的访问是尽可能一致的,例如通过使用madvise或fadvise调用(以及任何你可以改进访问模式的应用程序级别的更改)。

3你可以解决这个问题,例如,在一个较小的窗口中按顺序映射,比如100mb。

4事实上,MAP_POPULATE方法(至少在某些硬件/操作系统组合中)只比不使用它稍微快一点,可能是因为内核使用了faultaround——因此小故障的实际数量减少了16倍左右。