C++ 缓冲文本和有条件地将部分写入文本文件的有效方法

C++ efficient way to buffer text and conditionally write portions to a text file

我正在构建包含数亿行的 CSV 文本文件。每次调用 record 函数都会形成一行文本并将其缓冲到 stringstream 中。根据 record 函数的输入,缓冲行将定期写入文件或丢弃。我猜大多数时候大约有 75% 的缓冲行最终被写入文件。

所以,我真正在做的是形成一堆文本行,决定是将它们扔掉还是写入文件,然后一遍又一遍地重复多次。

下面是我的代码的一个简化示例。假设 CONDITION1CONDITION2 只是涉及 xyz 的简单布尔表达式;他们不会花很多时间来评估。代码真的很慢,我可以看出几个原因:一般使用 stringstreams,特别是重复调用 stringstream::str()stringstream::str(const string&)

问题:我怎样才能让它更快?

注意:我假设(或知道)使用 std::string 来保存一堆文本会更快,但我担心构建文本所需的额外转换使用 double 个变量,例如 x。 (在实际情况下,大约有 10 个不同的双精度变量以逗号分隔。)

std::ofstream outf;
stringstream ss;

// open outf

void record(const double x, const bool y, const int z) {
    ss << x << ", ";
    if(y) ss << "YES, ";
    else  ss << "NO, ";
    ss << z << "\n";

    if(CONDITION1) {
        if(CONDITION2)
            outf << ss.str();
        ss.str(std::string());
    }
}

背景

有两个根本问题导致了减速:

  1. 获取数据
  2. 正在解析数据

这些是通常占用最多时间的项目。

获取数据

获取数据的最佳过程是让数据一直流入内存。这可能意味着从读取大块数据到使用线程继续读取的任何事情。如果您正在使用硬盘驱动器,他们不喜欢停下来。他们有启动和定位扇区的开销。可以通过读取大块(每个请求更多数据)来减少启动。

正在解析数据

时间浪费在搜索分隔符和将文本表示转换为内部表示上。

固定字段长度解析起来最快。无需查找,数据在固定的字符位置。这消除了搜索分隔字符的时间。

此外,固定字段更容易从缓冲区进行处理。对于可变长度的记录,记录可能会跨越缓冲区的末尾,从而导致执行一些额外的代码。

减少分支

处理器更喜欢连续执行指令。当他们遇到分支指令时,他们会有点不高兴。这意味着他们必须从别处获取指令。条件更差。在条件得到解决之前,获取机器无法获取(尽管对获取算法进行了更多研究以加快它们的速度)。综上所述,减少瓶颈区域的分支数量。

简介。然后尝试其中的一些技巧。再次简介。比较 "before" 和 "after" 配置文件以确定增益。

假设这是可能的,第一个也是最明显的优化是在 进行任何转换之前检查条件。与此同时,您可以避免从字符串流的内容创建 string 对象,而只是直接从 stringstream 的缓冲区复制到 ostream 的缓冲区。我也可能使用向量来处理布尔转换。特别是对于不包括短字符串优化的实现,您还可以通过预初始化空字符串以用于清除字符串流来节省一些时间:

std::ofstream outf;
stringstream ss;

namespace {
    char const *names[] = { "No, ", "Yes, "};
    std::string clear;
}

// open outf

void record(const double x, const bool y, const int z) {

    if (!(CONDITION1 && CONDITION2))
        return;

    ss << x << ", ";
    ss << names[y];
    ss << z << "\n";

    outf << ss.rdbuf();
    ss.str(clear);
}

假设你真的可以像这样提前检查条件,你也可以完全消除 stringstream

void record(const double x, const bool y, const int z) {

    if (!(CONDITION1 && CONDITION2))
        return;

    outf << x << ", "
         << names[y]
         << z << "\n";
}

老实说,我怀疑这是否会产生巨大的影响,但这些是我在没有看到您真正关心的代码的情况下立即想到的最佳猜测。

放弃 iostreams。甚至不计算文件 I/O 本身,仅构建 stringstream 与您的磁盘应有的能力相比,已经将您的性能降低了一个数量级以上。 (See here for evidence).

使用 Jerry 的提前退出技巧,然后考虑使用普通 C 字符串处理函数构建缓冲区,例如 strncat(或负责防止缓冲区溢出的 "safe" 版本,这样您就不会不必)或 snprintf.

C++ iostreams 几乎可以胜任使用 filebuf 将准备好的缓冲区传输到磁盘,但您可能仍想针对 FILE* and/or OS 文件进行基准测试访问 API。

此外,在将几十条记录移交给磁盘之前,不要害怕将它们缓冲到应用程序级缓冲区中。