【问题标题】:Fast textfile reading in c++在 C++ 中快速读取文本文件
【发布时间】:2013-07-29 07:16:08
【问题描述】:

我目前正在用 C++ 编写一个程序,其中包括读取大量大型文本文件。每个都有约 400.000 行,在极端情况下,每行有 4000 个或更多字符。只是为了测试,我使用 ifstream 和 cplusplus.com 提供的实现读取了其中一个文件。花了大约60秒,这太长了。现在我想知道,有没有一种直接的方法可以提高阅读速度?

编辑: 我使用的代码或多或少是这样的:

string tmpString;
ifstream txtFile(path);
if(txtFile.is_open())
{
    while(txtFile.good())
    {
        m_numLines++;
        getline(txtFile, tmpString);
    }
    txtFile.close();
}

编辑 2:我读取的文件只有 82 MB 大。我主要说它可以达到4000,因为我认为可能需要知道才能进行缓冲。

编辑3:谢谢大家的回答,但鉴于我的问题,似乎没有太大的改进空间。我必须使用 readline,因为我想计算行数。将 ifstream 实例化为二进制文件也不会使读取速度更快。我会尽可能多地并行化它,至少应该可以。

编辑 4:显然有些事情我可以做到。非常感谢你花这么多时间在这件事上,我非常感谢! =)

【问题讨论】:

  • 使用随机归档还是顺序归档?向我们展示您的代码或您正在阅读的内容?
  • 很大程度上取决于你用它做什么。
  • 你可能想把它拆成碎片,因为对我来说这似乎是一个内存瓶颈 400000 行 * 4000 个字符可能是 1600000000 个字符,如果一个字符在你的系统上是 1 个字节,则可能是字节跨度>
  • 问题,您在实际代码中使用任何字符串流吗?
  • 速度很慢。只是检查..

标签: c++ performance io ifstream


【解决方案1】:

更新:请务必查看初始答案下方的(令人惊讶的)更新


内存映射文件对我很有帮助1

#include <boost/iostreams/device/mapped_file.hpp> // for mmap
#include <algorithm>  // for std::find
#include <iostream>   // for std::cout
#include <cstring>

int main()
{
    boost::iostreams::mapped_file mmap("input.txt", boost::iostreams::mapped_file::readonly);
    auto f = mmap.const_data();
    auto l = f + mmap.size();

    uintmax_t m_numLines = 0;
    while (f && f!=l)
        if ((f = static_cast<const char*>(memchr(f, '\n', l-f))))
            m_numLines++, f++;

    std::cout << "m_numLines = " << m_numLines << "\n";
}

这应该很快。

更新

如果它可以帮助您测试这种方法,这里有一个版本 using mmap 直接代替使用 Boost:see it live on Coliru

#include <algorithm>
#include <iostream>
#include <cstring>

// for mmap:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

const char* map_file(const char* fname, size_t& length);

int main()
{
    size_t length;
    auto f = map_file("test.cpp", length);
    auto l = f + length;

    uintmax_t m_numLines = 0;
    while (f && f!=l)
        if ((f = static_cast<const char*>(memchr(f, '\n', l-f))))
            m_numLines++, f++;

    std::cout << "m_numLines = " << m_numLines << "\n";
}

void handle_error(const char* msg) {
    perror(msg); 
    exit(255);
}

const char* map_file(const char* fname, size_t& length)
{
    int fd = open(fname, O_RDONLY);
    if (fd == -1)
        handle_error("open");

    // obtain file size
    struct stat sb;
    if (fstat(fd, &sb) == -1)
        handle_error("fstat");

    length = sb.st_size;

    const char* addr = static_cast<const char*>(mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0u));
    if (addr == MAP_FAILED)
        handle_error("mmap");

    // TODO close fd at some point in time, call munmap(...)
    return addr;
}

更新

通过查看 GNU coreutils wc 的源代码,我发现了最后一点性能。令我惊讶的是,使用以下改编自 wc 的(非常简化的)代码在上面的内存映射文件中运行的时间大约为 84%

static uintmax_t wc(char const *fname)
{
    static const auto BUFFER_SIZE = 16*1024;
    int fd = open(fname, O_RDONLY);
    if(fd == -1)
        handle_error("open");

    /* Advise the kernel of our access pattern.  */
    posix_fadvise(fd, 0, 0, 1);  // FDADVICE_SEQUENTIAL

    char buf[BUFFER_SIZE + 1];
    uintmax_t lines = 0;

    while(size_t bytes_read = read(fd, buf, BUFFER_SIZE))
    {
        if(bytes_read == (size_t)-1)
            handle_error("read failed");
        if (!bytes_read)
            break;

        for(char *p = buf; (p = (char*) memchr(p, '\n', (buf + bytes_read) - p)); ++p)
            ++lines;
    }

    return lines;
}

1 参见例如这里的基准:How to parse space-separated floats in C++ quickly?

【讨论】:

  • @ArneRecknagel 为了方便起见,它使用Boost Iostreams,但如果您愿意,您可以使用mmap (POSIX)MapViewOfFileEx function (Win32)
  • @ArneRecknagel 我添加了一个不使用 Boost 的版本以防万一。 See it live on Coliru(计算它自己的行数main.cpp
  • @ArneRecknagel 在使用 8.9GiB 文件进行基准测试后,我更新了我的代码。事实证明,使用 memchr 而不是 std::count 使其运行时间为 2.3 秒而不是 8.4 秒(快 3 倍以上)。接下来,在fd 上使用read 循环结果比使用mmap 稍快。我展示了我改编的wc() 版本here
  • 在您致电mmap() 后致电madvise(addr, 0, MADV_SEQUENTIAL) 是否有助于提高性能?这至少会使其与使用posix_fadvise()wc() 实现更具可比性。
  • 读取 16kiB 块会在您的进程中重用相同的 4 页地址空间。您不会有 TLB 未命中,并且 16kiB 小于 L1 缓存。来自页面缓存(在 read(2) 内)的 memcpy 运行得非常快,而 memchr 只涉及 L1 中的热内存。 mmap 版本必须对每个页面进行故障处理,因为mmap 不会连接所有页面(除非您使用 MAP_POPULATE,但当文件大小占 RAM 大小的很大一部分时,这将无法正常工作)。
【解决方案2】:

4000 * 400,000 = 1.6 GB 如果您的硬盘驱动器不是 SSD,您可能会获得约 100 MB/s 的顺序读取。仅在 I/O 中就是 16 秒。

由于您没有详细说明您使用的具体代码或您需要如何解析这些文件(您是否需要逐行读取它,系统是否有很多 RAM,您可以将整个文件读入一个大的 RAM 缓冲区,然后解析它?)您几乎无法加快处理速度。

按顺序读取文件时,内存映射文件不会提供任何性能改进。也许手动解析大块以获得新行而不是使用“getline”会提供改进。

编辑在做了一些学习之后(感谢@sehe)。这是我可能会使用的内存映射解决方案。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>

int main() {
    char* fName = "big.txt";
    //
    struct stat sb;
    long cntr = 0;
    int fd, lineLen;
    char *data;
    char *line;
    // map the file
    fd = open(fName, O_RDONLY);
    fstat(fd, &sb);
    //// int pageSize;
    //// pageSize = getpagesize();
    //// data = mmap((caddr_t)0, pageSize, PROT_READ, MAP_PRIVATE, fd, pageSize);
    data = mmap((caddr_t)0, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    line = data;
    // get lines
    while(cntr < sb.st_size) {
        lineLen = 0;
        line = data;
        // find the next line
        while(*data != '\n' && cntr < sb.st_size) {
            data++;
            cntr++;
            lineLen++;
        }
        /***** PROCESS LINE *****/
        // ... processLine(line, lineLen);
    }
    return 0;
}

【讨论】:

  • +1 用于计算啤酒杯垫。 SSD 可以达到 ~500Gb/s。根据使用场景,内存映射可能会更有效
  • 我需要逐行阅读,因为它们不包含告诉我它们有多长的标题。我可以将它们放入 RAM 缓冲区,因为我可以在阅读后丢弃每一个,但话又说回来,我认为这就是 ifstream 所做的。有没有办法告诉程序把整个东西扔进内存?
  • @sehe - 我一直认为内存映射文件更像是重叠 I/O 的一种方便抽象,而不是性能提升,尤其是对于顺序读取任务。我的猜测是 OP 正在使用“getline”,它一次读取 1 个字节以查找 \n 并导致大量不必要的小文件读取。在顺序 ifstream 中使用更大的读取缓冲区将提供与映射文件完全相同的性能(但我很愿意被证明是错误的)。
  • @ArneRecknagel - 如果您有足够的 RAM 来处理它,您可以获得文件大小并分配足够大的缓冲区并对缓冲区执行一次读取操作。这当然会有我提到的严重延迟,我更好的方法可能是分配一个 ~16MB 大小的缓冲区,读入它,解析你可以解析的行并将最后一行(此时可能无法解析)移动到开头缓冲区并继续你的读取循环到它的其余部分。
  • @LastCoder mmap 也很方便,而且:防止分页您不访问的所有页面,隐式以二进制模式工作,只需要 virtual 地址空间(如反对将其复制到本地缓冲区)。一些文件系统驱动程序甚至可能具有零拷贝路径,尤其是在只读映射上
【解决方案3】:

Neil Kirk,很遗憾,我无法回复您的评论(声誉不足),但我对 ifstream 和 stringstream 进行了性能测试,逐行读取文本文件的性能完全相同。

std::stringstream stream;
std::string line;
while(std::getline(stream, line)) {
}

对于 106MB 的文件,这需要 1426 毫秒。

std::ifstream stream;
std::string line;
while(ifstream.good()) {
    getline(stream, line);
}

在同一个文件上这需要 1433 毫秒。

以下代码反而更快:

const int MAX_LENGTH = 524288;
char* line = new char[MAX_LENGTH];
while (iStream.getline(line, MAX_LENGTH) && strlen(line) > 0) {
}

在同一个文件上这需要 884 毫秒。 这有点棘手,因为您必须设置缓冲区的最大大小(即输入文件中每一行的最大长度)。

【讨论】:

    【解决方案4】:

    作为有一点竞争编程背景的人,我可以告诉你:至少对于像整数解析这样简单的事情,C 中的主要成本是锁定文件流(这在默认情况下是为多线程完成的)。请改用unlocked_stdio 版本(fgetc_unlocked()fread_unlocked())。对于 C++,常见的知识是使用 std::ios::sync_with_stdio(false),但我不知道它是否和 unlocked_stdio 一样快。

    这里是我的标准整数解析代码供参考。它比 scanf 快 很多,正如我所说的,主要是因为没有锁定流。对我来说,它与我以前使用的最好的手动编码 mmap 或自定义缓冲版本一样快,而且没有疯狂的维护债务。

    int readint(void)
    {
            int n, c;
            n = getchar_unlocked() - '0';
            while ((c = getchar_unlocked()) > ' ')
                    n = 10*n + c-'0';
            return n;
    }
    

    (注意:这仅适用于任意两个整数之间恰好有一个非数字字符)。

    如果可能的话当然要避免内存分配...

    【讨论】:

      【解决方案5】:

      您必须同时读取所有文件吗? (例如在您的应用程序开始时)

      如果这样做,请考虑并行化操作。

      无论哪种方式,请考虑使用二进制流,或对数据块使用未提供的read

      【讨论】:

      • 在 HDD 上并行会使事情变得更糟,其影响取决于 HDD 上文件的分布。在 SSD 上,它可能(!)改进。
      • 您可能是对的(我没有考虑过单个 HDD 会导致进一步的延迟)。如果 op 结合无缓冲读取(例如 - 将 rdbuf() 移动到单独的 ostringstream 并从那里读取),它可能仍然更快)。我想一旦 op 决定实施,他(她?)将不得不衡量并找出答案。
      【解决方案6】:

      使用Random file access 或使用binary mode。对于顺序,这很大,但仍然取决于您正在阅读的内容。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多