mmap()与阅读块

时间:2020-03-05 18:48:16  来源:igfitidea点击:

我正在开发一个程序,该程序将处理可能大小为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中看起来会完全不同,因为Microsoft实现了一个漂亮的文件缓存,该缓存首先执行了mmap的大部分操作。即,对于频繁访问的文件,我们可以执行std :: ifstream.read(),它的速度与mmap一样快,因为文件缓存已经为我们完成了内存映射,并且它是透明的。

最终更新:我们好,人们:在操作系统和标准库以及磁盘和内存层次结构的许多不同平台组合中,我不能肯定地说,被称为黑匣子的系统调用mmap总是总是比"读取"要快得多。即使我的话可以这样解释,这也不是我的意图。最终,我的观点是,内存映射的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写一个C ++库(一个libgit ++,如果愿意的话),并且遇到了与此类似的问题:我需要能够打开大文件(大文件),并且性能不高(就像使用std :: fstream一样)。

" Boost :: Iostreams"已经有一个mapping_file源,但是问题是它是对整个文件进行" mmap"处理,这将我们限制为2 ^(wordsize)。在32位计算机上,4GB不够大。可以预期Git中的.pack文件会变得更大,这是不合理的,因此我需要不依赖常规文件I / O而分块读取文件。在" Boost :: Iostreams"的幕后,我实现了一个Source,它或者多或者少是std :: streambuf和std :: istream之间交互的另一种观点。我们也可以尝试类似的方法,只需将std :: filebuf继承到mapped_filebuf中,类似地,将std :: fstream继承成mapped_fstream。两者之间的相互作用很难正确解决。 " Boost :: Iostreams"已经为我们完成了一些工作,并且还为过滤器和链提供了挂钩,因此我认为以这种方式实现它会更有用。

回答

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

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

回答

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

回答

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

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

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

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

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

回答

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

我尝试了Ben Collins的代码片段(请参见上/下),以测试他的断言" 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不是我们计算机上虚拟内存的一页大小...恕我直言,仍然有文件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来提示应该从文件asap同步此内存。
我希望MAP_POPULATE为mmap提供正确的提示(即,它不会在调用返回之前尝试加载所有内容,但会使用feed_data异步进行加载)。至少它使用此标志提供了更好的结果,即使手册指出自2.6.23起没有MAP_PRIVATE也不执行任何操作。