什么是随机化非常大的文件内容排序的有效方法?

时间:2018-02-17 22:47:18

标签: c++ io mmap large-files random-access

对于我的神经网络训练项目,我有一个非常大的输入数据文件。文件格式是二进制文件,它包含大量固定大小的记录。该文件目前约为13GB,但未来可能会变大;出于这个问题的目的,我们假设它太大而不能立即将所有内容全部保存在我的计算机内存中。

今天的问题涉及我写的一个小实用程序(在C ++中,虽然我认为语言选择在这里并不重要,因为人们可能会遇到任何语言中的相同问题)读取大文件并输出一个类似的大文件 - 输出文件包含与输入文件相同的数据,除非记录随机排序。

为此,我mmap()将输入文件存入内存,然后生成从1到N的整数列表(其中N是输入文件中的记录数),随机改变该列表的顺序,然后遍历列表,将输出文件写入mmap内存区域的第n条记录。

这一切都正常,就此而言;问题是它不能很好地扩展;也就是说,随着输入文件的大小变大,进行此转换所需的时间比O(N)增加得更快。它已经到了我的工作流程瓶颈的地步。我怀疑问题是I / O系统(对于MacOS / X 10.13.4,使用我的Mac Pro垃圾桶的内部SSD,如果重要的话)针对顺序读取进行了优化,并且完全跳转到了就缓存/预读/其他I / O优化而言,输入文件中的随机位置几乎是最糟糕的情况。 (我想在旋转的磁盘上由于头部搜索延迟会更糟糕,但幸运的是我至少在这里使用SSD)

所以我的问题是,是否有任何聪明的替代策略或优化我可以使这个文件随机化过程更有效 - 随着输入文件的大小增加,可以更好地扩展?

3 个答案:

答案 0 :(得分:1)

如果在读取随机文件位置时问题与交换和随机磁盘访问有关,您是否至少可以按顺序读取输入文件?

当您访问mmap-ed文件中的某个块时,prefetcher会认为您很快就会需要相邻的页面,因此它也会加载它们。但你不会,所以这些页面将被丢弃,加载时间将被浪费。

  • 创建N toPositons的数组,所以toPosition [i] = i;
  • 随机化目的地(你使用knuth的shuffle吗?);
  • 然后toPosition [i] =输入[i]的目的地。因此,从开始按顺序读取输入数据并将它们放入目标文件的相应位置。

也许,这对预告员更友好。当然,随机写入数据也很慢,但至少,你不会浪费输入文件中的预取页面。

额外的好处是,当您处理了数百万个输入数据页时,这些GB将从RAM中卸载,您将永远不再需要它们,因此您不会污染实际的磁盘缓存。请记住,实际内存页面大小至少为4K,因此即使您随机访问1字节的mmap-ed文件,也应该至少将4K数据从磁盘读入缓存。

答案 1 :(得分:1)

我建议不要使用mmap() - 根本不存在所有内存压力,unless you're re-reading the same data multiple times, mmap() is often the worst-performing way to read data

首先,生成N个随机偏移量,然后,给定这些偏移量,使用pread()读取数据 - 并使用低级C风格IO。

这会将fcntl() function to disable the page cache用于您的文件。由于您不会重新读取相同的数据,因此页面缓存可能对您没什么好处,但它会耗尽RAM,从而减慢其他因素。无论是否禁用页面缓存,都可以尝试使用它,看看哪个更快。另请注意,我遗漏了所有错误检查:

(我还假设C风格的IO函数在MAC上的namespace std中,并且我使用C风格的字符串和数组来匹配C风格的IO函数,同时保持代码更简单。)

#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>

void sendRecords( const char *dataFile, off_t offsets, size_t numOffsets )
{
    int fd = std::open( dataFile, O_RDONLY );
    // try with and without this
    std::fcntl( fd, F_NOCACHE, 1 );

    // can also try using page-aligned memory here
    char data[ RECORD_LENGTH ];

    for ( size_t ii = 0; ii < numOffsets; ii++ )
    {
        ssize_t bytesRead = std::pread( fd, data, sizeof( data ), offsets[ ii ] );
        // process this record
        processRecord( data );
    }

    close( datafd );
}

假设您有一个包含预先计算的随机偏移的文件:

#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>

void sendRecords( const char *dataFile, const char *offsetFile )
{
    int datafd = std::open( dataFile, O_RDONLY );
    // try with and without this
    std::fcntl( fd, F_NOCACHE, 1 );

    int offsetfd = std::open( offsetFile, O_RDONLY );

    // can also try using page-aligned memory here
    char data[ RECORD_LENGTH ];

    for ( ;; )
    {
        off_t offset;
        ssize_t bytesRead = std::read( offsetfd, &offset, sizeof( offset ) );
        if ( bytesRead != sizeof( offset ) )
        {
            break;
        }
        bytesRead = std::pread( fd, data, sizeof( data ), offset );
        // process this record
        processRecord( data );
    }

    std::close( datafd );
    std::close( offsetfd );
}

您也可以更快,因为该代码交替进行读取和处理,并且使用多个线程同时读取和处理可能更快。使用一个或多个线程将数据读入预分配的缓冲区然后排队并发送到处理线程并不难。

答案 2 :(得分:0)

感谢各个人在这个帖子中的建议(特别是Marc Glisse和Andrew Henle),我能够在13GB输入文件上减少程序的执行时间,从大约16分钟到大约2分钟。我将在这个答案中记录我是如何做到的,因为解决方案与上述任何一个答案都不太相似(它更基于Marc的评论,所以如果/当他重述他的评论时我会给Marc复选框作为答案)。

我尝试用pread()替换mmap()策略,但这似乎没有太大区别;我尝试将F_NOCACHE和其他各种标志传递给fcntl(),但它们似乎没有效果或使事情变慢,所以我决定尝试不同的方法。

新方法是以两层方式执行操作:我的程序现在不是一次读取单个记录,而是从输入文件中加载顺序记录的“块”(每个块包含大约4MB的数据) )。

块以随机顺序加载,我加载块直到我在RAM中保存了一定数量的块数据(目前约为4GB,因为这是我的Mac可以轻松容纳的)。然后我开始从随机的RAM块中抓取随机记录,并将它们写入输出文件。当给定的块不再有任何记录要抓取时,我释放该块并从输入文件加载另一个块。我重复此操作,直到输入文件中的所有块都已加载并且所有记录都分发到输出文件。

这更快,因为我的所有输出都是严格顺序的,我的输入大多是顺序的(即每次搜索后读取4MB数据而不是仅约2kB)。输出的顺序稍微不那么随机,但我不认为这对我来说是个问题。