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

What's an efficient way to randomize the ordering of the contents of a very large file?

本文关键字:有效 方法 是什么 排序 文件 随机化      更新时间:2023-10-16

对于我的神经网络训练项目,我有一个非常大的输入数据文件。文件格式是二进制的,它由大量的固定尺寸记录组成。该文件当前〜13GB,但将来可能会变得更大。出于这个问题的目的,让我们假设它太大了,无法立即将所有内容放在我的计算机的RAM中。

今天的问题涉及我编写的一个小实用程序(在C 中,尽管我认为语言的选择在这里并不重要,因为在任何语言中可能会遇到相同的问题(,旨在阅读大文件和输出一个类似的大文件 - 输出文件将包含与输入文件相同的数据,除了记录被随机订购。

为此,i mmap()输入文件到内存中,然后生成一个从1到n的整数列表(其中n是输入文件中的记录数(,随机洗牌该列表的排序,然后迭代列表,从mmap'd内存区域写入输出文件。

这一切都可以正常工作。问题在于它的扩展不佳。也就是说,随着输入文件的大小变大,进行此转换所需的时间比O(n(更快。它已经成为我工作流程的瓶颈的地步。我怀疑问题是I/O系统(对于MacOS/X 10.13.4,使用Mac Pro垃圾桶的内部SSD,以防万一很重要的情况下(进行了优化,以用于顺序读取,并跳到完全随机的位置就缓存/读取/其他I/O优化而言,输入文件几乎是最坏的情况。(我想在旋转磁盘上,由于远见的延迟,它的性能甚至会更糟,但是幸运的是,我至少在这里使用SSD(

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

<</p>

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

当您访问MMAP-ED文件中的一些块时,Prefetcher会认为您很快就需要相邻页面,因此它也会加载它们。但是您不会,因此这些页面将被丢弃,并且将浪费加载时间。

  • 创建n个topositons的数组,因此sposition [i] = i;
  • 随机范围(您使用Knuth的洗牌吗?(;
  • 然后置换[i] =输入[i]的目的地。因此,从start依次读取输入数据,然后将它们放入目标文件的相应地点。

也许,这将更加友好。当然,随机编写数据也很慢,但是至少您不会从输入文件中浪费预取的页面。

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

我建议不要使用 mmap()-根本没有任何记忆压力,除非您重新阅读相同的内容数据多次,mmap()通常是读取数据的最糟糕的方法。

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

这使用fcntl()函数禁用文件的页面缓存。由于您没有重新阅读相同的数据,因此页面缓存可能不错,但是它确实耗尽了RAM,从而减慢了其他内容。在有或没有页面缓存禁用的情况下,尝试一下,然后查看哪个更快。还要注意,我遗漏了所有错误检查:

(我还假设C风格的IO函数在Mac上在namespace std中,并且我使用C风格的字符串和数组来匹配C-Style 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 );
}

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

多亏了此线程中各个人的建议(尤其是Marc Glisse和Andrew Henle(,我能够减少在13GB输入文件上的程序的执行时间,从〜16分钟开始到〜2分钟。我会在这个答案中记录我如何做到这一点,因为该解决方案不像上面的任何一个答案(这更基于Marc的评论,所以我会给Marc复选框,如果/当他重申评论时作为答案(。

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

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

块以随机顺序加载,我将块加载到块中,直到我在RAM中持有一定数量的块数据(目前〜4GB,因为这是我的Mac的RAM可以舒适地保持的(。然后,我开始从随机的RAM块中获取随机记录,然后将其写入输出文件。当给定的块不再留下任何记录要抓住时,我将释放该块并从输入文件中加载另一个块。我重复此操作,直到已加载输入文件中的所有块并将其所有记录分发到输出文件。

这是更快的,因为我所有的输出都是严格的顺序,并且我的输入主要是顺序的(即,在每个搜索之后而不是仅〜2kb之后读取4MB数据(。输出的顺序比以前的序列稍小,但我认为这对我来说不会是问题。