C++写入CSV文件,性能

5
这可能是一个简单的问题,但我找不到关于它的具体信息,或者至少没有可读的信息。我找到的大部分信息都涉及从 .csv 文件中读取数据。
我有一个函数,需要将数据保存到 .csv 文件中。从性能角度来看,这不是一种理想的格式,但让我们假设这不能改变。我的数据存储在一个r x c x s 数据结构中,并且必须以 r,c,s,value 的形式输出并保存到 .csv 文件中。目前我有以下代码:
char delimiter = ',';
ofstream ofs(file, ofstream::out);

for (int r = 0; r < P.n_rows; r++)
{
    for (int c = 0; c < P.n_cols; c++)
    {
        for (int s = 0; s < P.n_slices; s++)
        {
            ofs << r + 1 << delimiter << c + 1 << delimiter << s + 1 << delimiter << P(c, s, s) << endl;
            count++;
        }
    }
}
ofs.close();

对于一个大小为100x100x50的数据结构,这需要大约6秒的时间,我认为这是一个必要的长时间。若您能提供一些关于如何加速的信息,我将不胜感激。


那里一定有很多CSV写入库。你试过任何一个,如果它给了你更好的结果吗?P的运行时间占比有多大? - Kijewski
8
首先,放弃使用 endl。你真的不需要每一行都刷新。 - BoBTFish
我还没有尝试过任何CSV库,但我会去看看是否能找到有用的东西。 - Mattias Svensson
这个拼写错误 P(c, s, s)(切片使用两次)只是在这篇文章中还是你的代码中也有? - Vlad Feinstein
哦,我明白了。实际上上面的例子里只是一个错别字。应该是 P(r, c, s) - Mattias Svensson
3个回答

13

请注意,endl 不仅仅是一个换行符,它实际上将数据刷新到磁盘。

向输出序列 os 插入一个换行符并将其刷新,就好像调用 os.put(os.widen('\n')) 然后调用 os.flush() 一样。

这可能会显著减慢运行速度。您应该尝试将其替换为换行符。


感谢回复,在这种特定情况下,将 endl 替换为 \n 在性能方面有了大约 50-60% 的提升。 - Mattias Svensson
非常感谢。祝你速度更快! - Ami Tavory
3
为什么刷新操作比写入操作快很多?因为刷新操作会立即将数据发送到流目标,但是写入硬盘非常慢,所以文件流使用缓冲区先在RAM中收集数据,并且较少地推送更大的数据块。这些块甚至会被调整为适合硬盘的大小。即使清空缓冲区也可能会花费一些时间。最好让流使用完整的缓冲区并在关闭时自动刷新。标准流总是刷新,因为用户希望立即看到数据而不需要等待。 - Youka

3

如上所述(并得到接受),删除endl可将时间缩短50-60%(在我的情况下,从7秒缩短到2秒,超过70%)。

但是,仍有改进的空间:通用流格式化。以下代码可以将运行时间再次缩短75%,至500毫秒:

int a[100][100][50];
int main(int argc, char** argv)
{
    char buff[64];
    memset(a, 1, 100 * 100 * 50 * sizeof(int));
    int count(0);
    char delimiter = ',';
    auto start = std::chrono::steady_clock::now();
    std::ofstream ofs("test.csv", std::ofstream::out);
    for (int r = 0; r < 100; r++)
    {
        for (int c = 0; c < 100; c++)
        {
            for (int s = 0; s < 50; s++)
            {
                sprintf_s(buff, "%d,%d,%d,%d\n", r, c, s, a[c][r][s]);
                ofs << buff;
                //ofs << r + 1 << delimiter << c + 1 << delimiter << s + 1 << delimiter << a[c][r][s] << '\n';
                count++;
            }
        }
    }
    ofs.close();
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << " ms" << endl;
    return count;
}

1

首先将内容写入stringstream,然后将其全部写入输出文件:

char delimiter = ',';
stringstream ss;

for (int r = 0; r < P.n_rows; r++)
{
    for (int c = 0; c < P.n_cols; c++)
    {
        for (int s = 0; s < P.n_slices; s++)
        {
            ss << r + 1 << delimiter << c + 1 << delimiter << s + 1 << delimiter << P(r, c, s) << endl;
        }
    }
}
ofstream ofs(file, ofstream::out);
ofs << ss.str();
ofs.close();

实际上,将 endl 替换为“\n”已经解决了这个问题:

char delimiter = ',';
ofstream ofs(file, ofstream::out);

for (int r = 0; r < P.n_rows; r++)
{
    for (int c = 0; c < P.n_cols; c++)
    {
        for (int s = 0; s < P.n_slices; s++)
        {
            ofs << r + 1 << delimiter << c + 1 << delimiter << s + 1 << delimiter << P(r, c, s) << "\n";
        }
    }
}
ofs.close();

这在我的机器上可以提升约2倍的速度:

string delimiter = ",";
string ss;
ss.reserve(P.n_rows * P.n_cols * P.n_slices * 20);

int max_idx = max(P.n_rows, max(P.n_cols, P.n_slices));
vector<string> idx_str(max_idx);
for(int i=0;i<max_idx;++i) idx_str[i] = std::to_string(i+1);

for (int r = 0; r < P.n_rows; r++)
{
  auto& rstr = idx_str[r];
    for (int c = 0; c < P.n_cols; c++)
    {
      auto& cstr = idx_str[c];
      string thisline = rstr + delimiter + cstr + delimiter;

        for (int s = 0; s < P.n_slices; s++)
        {
          auto& sstr = idx_str[s];
            ss += thisline
                + sstr + delimiter
                + std::to_string(data[r][c][s]) + "\n";
        }
    }
}
ofstream ofs(file, ofstream::out);
ofs.write(ss.c_str(), sizeof(char)*ss.size());
ofs.close();

1
你为什么认为这样会更快呢?(并不是完全反对,但一个好的回答应该解释清楚,并包含一些证据)。这会增加很多分配开销。另外,为什么要使用 std::stringstream 而不是 std::ostringstream,以及为什么要刷新它,这根本没有任何有用的作用? - BoBTFish
1
基本上只是将许多磁盘IO操作聚合成一个。根据我的经验,速度可能会快5倍到10倍。 - phg1024
一个好的观点,应该成为答案的一部分,而不是评论。并且,通过简单地删除原始代码中多余的刷新操作,可以更简单地解决这个问题。 - BoBTFish
刷新只是为了保持与原始代码相同的输出格式。 - phg1024
2
但是冲洗本来就是问题所在!(可能) - BoBTFish
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接