【问题标题】:What's an efficient way to randomize the ordering of the contents of a very large file?随机化非常大文件内容的顺序的有效方法是什么?
【发布时间】:2018-02-17 22:47:18
【问题描述】:

对于我的神经网络训练项目,我有一个非常大的输入数据文件。文件格式是二进制的,它由大量固定大小的记录组成。该文件目前约为 13GB,但将来可能会变得更大;出于这个问题的目的,我们假设它太大而不能一次将所有这些都保存在我的计算机的 RAM 中。

今天的问题涉及我编写的一个小实用程序(用 C++,虽然我认为语言的选择在这里并不重要,因为在任何语言中都可能遇到相同的问题),旨在读取大文件和输出一个类似的大文件——输出文件包含与输入文件相同的数据,除了将记录打乱成随机顺序。

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

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

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

【问题讨论】:

  • 数据库?
  • 对于这种用途,您可能不需要完全统一的排列选择,因此您可以使用适合内存的块。
  • 我能看到的最佳解决方案是让使用随机文件的进程从非随机文件中进行挑选 - 换句话说,永远不要创建随机文件。
  • @NeilButterworth 这不只是解决问题吗?如果我这样做了,我认为使用该文件的进程会运行得很慢,原因与当前进程运行缓慢的原因相同......
  • 如果文件在 SSD 上,即使它针对顺序读取进行了优化,读取的数据越多,它也不会变得越来越慢。您是否尝试过使用mmap()Just use pread() 另外,这里的一些想法可能会有所帮助:stackoverflow.com/questions/2299402/…

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


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

    我建议不要使用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 );
    }
    

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

    【讨论】:

      【解决方案3】:

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

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

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

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

      这更快,因为我的所有输出都是严格顺序的,而且我的输入大多是顺序的(即每次查找后读取 4MB 的数据,而不是仅约 2kB)。输出的顺序比原来的随机性稍差,但我认为这对我来说不是问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-02-01
        • 1970-01-01
        • 2023-04-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多