【问题标题】:Most efficient way to parse every fourth line from a very large file从一个非常大的文件中解析每四行的最有效方法
【发布时间】:2015-10-17 18:42:33
【问题描述】:

我有一个格式如下的文件:

1: some_basic_info_in_this_line
2: LOTS_OF_INFO_IN_THIS_LINE_HUNDREDS_OF_CHARS
3: some_basic_info_in_this_line
4: LOTS_OF_INFO_IN_THIS_LINE_HUNDREDS_OF_CHARS
...

这种格式会重复数万次,使文件达到 50 GiB+。我需要一种有效的方法来处理这种格式的唯一第 2 行。我愿意使用 C、C++11 STL 或 boost。我已经查看了有关 SO 上文件流式传输的各种其他问题,但我觉得我的情况很独特,因为文件很大,而且每四行只需要一行。

内存映射文件似乎是我读过的最有效的文件,但映射 50+ GB 的文件会占用大多数计算机的 RAM(您可以假设此应用程序将由“普通”用户使用 - 比如说4-8 GiB RAM)。此外,我一次只需要处理其中一条线。以下是我目前的做法(是的,我知道这效率不高,这就是我重新设计它的原因):

std::string GL::getRead(ifstream& input)
{
    std::string str;
    std::string toss;
    if (input.good())
    {
        getline(input, toss);
        getline(input, str);
        getline(input, toss);
        getline(input, toss);
    }
    return str;
}

将 mmap 分解成块是否适合我的情况?无论如何,我可以利用 4 行中的 1 行吗?感谢您的帮助。

【问题讨论】:

  • 再一次,我想知道谁对此投了反对票。这是一个足够有趣的问题,而且提出得很好。 +1
  • 如果您的格式严格定义为行大小,您可以使用 input.seekg 跳过不需要的行
  • 主要瓶颈将在输入操作本身。我建议您尝试各种方法在典型用户的计算机上逐块加载数据。不知道如何最好地处理跨越两个街区的线(大多数街区都会有这样的线)。
  • 在返回str之前检查输入操作是否成功通常是个好主意——调用者应该检查输入中是否设置了任何标志,它只是eof而不是badfail,但还要在某个阶段检查eof - 仅在输入之前让getRead() 检查input.good() - 如果其余部分正确完成,则该检查应该是多余的。
  • 就像 Nir ​​说的,你必须检查每个字符是否是换行符,除非你可以根据文件格式的规则跳过一些。 strchr 或任何用于内部寻找换行符的东西都应该能够轻松跟上磁盘读取带宽,即使是从体面的 SSD。

标签: c++ performance c++11 boost


【解决方案1】:

使用ignore 代替getline

std::string GL::getRead(ifstream& input)
{
    std::string str;
    if (!input.fail())
    {
        input.ignore(LARGE_NUMBER, '\n');
        getline(input, str);
        input.ignore(LARGE_NUMBER, '\n');
        input.ignore(LARGE_NUMBER, '\n');
    }
    return str;
}

LARGE_NUMBER 可能是 std::numeric_limits<std::streamsize>::max(),如果您没有充分的理由使用较小的数字(想想 DOS 攻击)

提示考虑通过引用传递str。通过每次读取相同的字符串,您可以避免大量分配,这通常是程序运行缓慢的第一大原因。

提示考虑使用内存映射文件(Boost Iostreams、Boost Interpocess 或 mmap(1)

【讨论】:

  • 我会检查getline 调用是否成功。不确定它是否保证失败时为空字符串。
  • LARGE_NUMBER 应该std::numeric_limits<std::streamsize>::max(),这是一种特殊情况,告诉ignore不要计算字符,只是为了寻找分隔符。
  • @Blastfurnace 已修复(尽管我可能会在这里使用其他东西更安全。你不想有意外失控的 IO)
  • @sehe:如果您要使用非无限的最大计数,则为 getline (它将分配内存以保存整行)比为 @sehe 更有意义987654330@ 只是跳过字符。这是防止您的程序在/dev/zero 上运行的错误地方。
  • @sehe:我的观点是,在忽略循环中以较小的块工作并不能防止无限循环行为。我认为我们都同意正确的选择取决于您对输入的控制。例如如果文件的开头看起来没问题,并且它的大小是有限的,那么对文件的其余部分进行错误检查可以保持相当简单。如果您在潜在的恶意文件上运行此程序,则需要在此过程中对所有行进行早期检查,尤其是。你用getline阅读的那个。
【解决方案2】:

内存映射文件不会将其加载到 RAM 中。它占用进程的虚拟地址空间,但不占用物理 RAM。 mmap 系统调用在 32 位系统上只会失败,因为 4GiB 的虚拟地址空间不足以容纳 50GiB 的文件。在 64 位系统上,这将需要几微秒。 (不读取磁盘,因为文件已经打开,所以文件元数据已经加载。)

只有您实际读取的页面才会从磁盘加载,并且只要操作系统想要回收一些内存,就可以再次取消映射页面。 (因为如果您稍后再次读取它们,操作系统可以从磁盘重新加载。这就像将它们交换到交换空间/页面文件,但无需写入,因为磁盘上已经有一个干净的副本。)

内存映射可让您的进程读取操作系统的页面缓存页面,而不是使用read 系统调用复制它们。

Have a look at wikipedia 了解更多信息。

【讨论】:

    【解决方案3】:

    这是我能想到的最有效的解决方案,它独立于平台。我考虑了所有用户,现在假设每个人都有一台 64 位机器,如果他们使用 4+ GiB 文件大小。如果这种情况发生变化,我将不得不修改类以将数据“块”处理到单独的 mmap 区域中。

    #include <string>
    #include <boost/iostreams/device/mapped_file.hpp>
    
    //////////////////////////////////////////////////////////////////////////////////////////
    /// @class LineParser
    ///
    /// @brief Class to efficiently parse a file and take the second line out of every 4 lines
    ///
    /// This class uses memory-mapped io to efficiently extract and return sequences from a
    /// file
    //////////////////////////////////////////////////////////////////////////////////////////
    class LineParser
    {
    private:
        boost::iostreams::mapped_file mmap; ///< Object for memory mapped file
        const char* curr;                   ///< Current position of the file
        const char* end;                    ///< End position of the file
    
    public:
        //////////////////////////////////////////////////////////////////////////////////////
        /// @fn valid
        ///
        /// Indicates whether the parser is in a valid state or not
        ///
        /// @return Boolean indicating if the parser is open and in a valid state
        ///
        /// @note Declared inline as it is acceptable in my situation because of being called 
        ///       many times in only a few spots    
        //////////////////////////////////////////////////////////////////////////////////////
        inline bool valid(void)
        {
            return (curr && end && (curr < end) && (mmap.is_open()));
        }
    
        //////////////////////////////////////////////////////////////////////////////////////
        /// @fn peek
        ///
        /// Obtains the next sequence string - if it exists - but maintains parsers state
        ///
        /// @return Next sequence available in the file. Emptry string returned if none 
        ///         exist
        ///
        /// @note Declared inline as it is acceptable in my situation because of being called 
        ///       many times in only a few spots
        //////////////////////////////////////////////////////////////////////////////////////
        inline std::string peek(void)
        {
            const char* save = curr;
            std::string ret;
    
            getRead(ret);
    
            curr = save;
            return ret;
        }
    
        //////////////////////////////////////////////////////////////////////////////////////
        /// @fn getRead
        ///
        /// Sets container to the current read being processed
        ///
        /// @param container String container to place current sequence into
        ///
        /// @return Boolean indicating if getting a new read was successful
        ///
        /// @note Declared inline as it is acceptable in my situation because of being called 
        ///       many times in only a few spots
        //////////////////////////////////////////////////////////////////////////////////////
        inline bool getRead(std::string& container)
        {
            if (valid() == false)
            {
                return false;
            }
    
            curr = static_cast<const char*>(memchr(curr, '\n', end-curr)) + 1;
            const char* index = static_cast<const char*>(memchr(curr, '\n', end-curr));
            container = std::string(curr, index - curr);
            curr = index + 1;
            curr = static_cast<const char*>(memchr(curr, '\n', end-curr)) + 1;
            curr = static_cast<const char*>(memchr(curr, '\n', end-curr)) + 1;
    
            return true;
        }
    
        //////////////////////////////////////////////////////////////////////////////////////
        /// @fn LineParser
        ///
        /// Constructor to initialize memory mapped file and set index values
        //////////////////////////////////////////////////////////////////////////////////////
        LineParser(const std::string& filepath) 
            : mmap(filepath, boost::iostreams::mapped_file::readonly)
        {
            if (mmap.is_open())
            {
                curr = mmap.const_data();
                end = curr + mmap.size();
            }
        }
    
        //////////////////////////////////////////////////////////////////////////////////////
        /// @fn ~LineParser
        ///
        /// Default destructor
        //////////////////////////////////////////////////////////////////////////////////////
        ~LineParser(void) = default;
    };
    

    请注意,这个类并不完美,可能无法很好地处理文件格式的偏差,但在完美文件格式的假设下它可以很好地工作。

    【讨论】:

      猜你喜欢
      • 2023-03-25
      • 1970-01-01
      • 2017-03-06
      • 2018-12-31
      • 2013-02-09
      • 2019-03-11
      • 2021-09-21
      • 2012-05-28
      • 1970-01-01
      相关资源
      最近更新 更多