提高/优化C++文件写入速度

11

我在写文件时遇到了一些问题-主要是无法写入得足够快。

为了解释清楚,我的目标是捕获通过千兆以太网传入的数据流并将其简单保存到文件中。

原始数据以10MS/s的速率传入,然后保存到缓冲区,随后写入文件。

下面是相关代码的部分内容:

    std::string path = "Stream/raw.dat";
    ofstream outFile(path, ios::out | ios::app| ios::binary);

    if(outFile.is_open())
        cout << "Yes" << endl;

    while(1)
    {
         rxSamples = rxStream->recv(&rxBuffer[0], rxBuffer.size(), metaData);
         switch(metaData.error_code)
         {

             //Irrelevant error checking...

             //Write data to a file
                std::copy(begin(rxBuffer), end(rxBuffer), std::ostream_iterator<complex<float>>(outFile));
         }
    } 

我遇到的问题是将样本写入文件的时间太长了。大约过了一秒钟,发送样本的设备报告其缓冲区已溢出。通过对代码进行快速分析,几乎所有执行时间都花费在std::copy(...)上(确切地说是99.96% 的时间)。如果我删除这行代码,则可以运行程序数小时而不遇到任何溢出。
话虽如此,我还是对如何提高写入速度感到困惑。我查看了这个网站上的几篇文章,似乎最常见的建议(关于速度)是实现文件写入,就像我已经做的那样——通过使用std::copy
如果有帮助的话,我正在Ubuntu x86_64上运行此程序。欢迎提出任何建议。

2
这是关于USRP的,不是吗? - Marcus Müller
有趣的是...纯C指针式的方向可能会更好。如果你了解操作系统的结构,你可能能够更快地访问内存。 - A. Abramov
1
std::copy是逐元素复制的吗?这是IO时常犯的错误,非常慢。 - usr
添加了USRP和软件定义无线电标签,因为它们适用于此处。未能获得实时处理所需的整体系统性能是一个非常普遍的问题。 - Marcus Müller
@A.Abramov UHD,Mlagma使用的设备接口是C++(它有一个全新的C包装器,但这不如原始/基础的C++实用,也不更快)。 - Marcus Müller
显示剩余4条评论
2个回答

13

这里的主要问题是您试图在接收数据和写入数据的相同线程中进行写入,这意味着只有在复制完成后才能再次调用recv()。以下是一些观察点:

  • 将写入操作移到另一个线程。由于这涉及到USRP,因此GNU Radio可能确实是您的选择工具——它本质上是多线程的。
  • 您的输出迭代器可能不是最有效的解决方案。直接向文件描述符“write()”可能更好,但这是由您进行性能测量的决定。
  • 如果您的硬盘/文件系统/操作系统/CPU无法处理来自USRP的速率,即使将接收和写入分离成不同的线程,那么您无能为力——需要升级更快的系统。
  • 尝试写入RAM磁盘。

实际上,我不知道您是如何想出使用std::copy方法的。rx_samples_to_file example that comes with UHD使用简单的写入方式进行,您应该绝对优先考虑使用这种方式而不是复制方式;在良好的操作系统上,文件I/O通常可以少做一次复制,而且遍历所有元素可能非常缓慢。


3
同意,补充一下:将接收到的数据写入一个或多个大型缓冲区(取决于接收数据和写入文件之间的延迟时间)。创建一个线程从该缓冲区读取数据,并以“大块”的方式写入文件。此外,尽可能利用硬件辅助功能,如DMA。 - Thomas Matthews
@ThomasMatthews 我按照你的建议改用了 write,效果非常好 - 它还没有溢出。我也增加了缓冲区的大小。 - Mlagma
@ThomasMatthews 我目前正在开发一个多线程应用程序。实际上,我不会在这个函数中编写文件。目前只是为了证明概念而已。最近我刚刚完成了一个多线程缓冲区,用于将样本传递到程序的不同部分,例如解调等等。 - Mlagma
@usr:为了更好地解释,如果缓冲区是任意灵活的,吞吐量将是唯一重要的度量标准,那么你的评论就是正确的。然而,问题在于这两个假设都不成立:延迟很重要,因为在通过千兆以太网(或 USB3、10GigE 或 PCIe,这些是当前非嵌入式 USRP 的接口)传入的数据包被缓冲一段时间后,缓冲区就会变满,操作系统和硬件被迫丢弃数据。这是一场灾难!所以,不能指望操作系统“猜测”应用程序需要什么架构。 - Marcus Müller
@usr:实际经验表明,即使使用SSD的条带化RAID以获得高速率,这种假设也是错误的。别误会,如果你通常进行数据处理,那么你的假设是正确的,但这是实时世界,在这里,PC不仅仅使用“平均速率”,而是使用短期延迟,这些延迟很容易变得太大。顺便说一下,UHD是UDP(TCP没有意义,几毫秒延迟的数据包已经没有用了)。OP已经增加了缓冲区大小。真的,你的假设与现实不符。 - Marcus Müller
显示剩余5条评论

5

让我们来算一下。

你的样本显然是std::complex<std::float>类型的。给定一个典型的32位浮点数,这意味着每个样本占用64位。在10 MS / s下,原始数据大约为80兆字节/秒——这在你可以写入台式机(7200 RPM)硬盘的范围内,但接近极限(通常约为100-100兆字节/秒)。

不幸的是,尽管使用了std::ios::binary,但实际上您正在以文本格式(因为std::ostream_iterator基本上执行stream << data;)编写数据。

这不仅会丢失一些精度,而且会增加数据的大小,至少作为规则。增加的确切数量取决于数据--小的整数值实际上可以减少数据量,但对于任意输入,大小接近2:1的增加是相当普遍的。随着2:1的增长,您的传出数据现在约为160兆字节/秒——比大多数硬盘的处理速度更快。

改进的明显起点将是以二进制格式编写数据:

uint32_t nItems = std::end(rxBuffer)-std::begin(rxBuffer);
outFile.write((char *)&nItems, sizeof(nItems));
outFile.write((char *)&rxBuffer[0], sizeof(rxBuffer));

目前为止,我使用了sizeof(rxBuffer),假设它是一个真正的数组。如果它实际上是一个指针或向量,您需要计算正确的大小(您想要写入的总字节数)。

我还要指出,就目前而言,您的代码有一个更严重的问题:由于它在写入数据时没有指定元素之间的分隔符,因此数据将被写入而没有任何分隔一个项与下一个项。这意味着,如果您写入两个值(例如)10.2,则读取回来的不是10.2,而是一个单独的值10.2。在文本输出中添加分隔符将增加更多开销(约为15%的数据),因为该过程已经失败,因为它生成的数据太多。

以二进制格式写入意味着每个浮点数将精确地消耗4个字节,因此不需要分隔符才能正确读取数据。

接下来是降低到较低级别的文件I/O例程。根据情况,这可能或可能不会产生很大差异。在Windows上,您可以在使用CreateFile打开文件时指定FILE_FLAG_NO_BUFFERING。这意味着对该文件的读取和写入将基本上绕过缓存并直接到达磁盘。

在您的情况下,这可能是一个胜利——在10 MS/s的速度下,您可能会用完缓存空间,而不是在重新读取相同的数据之前。在这种情况下,让数据进入缓存几乎没有带来任何好处,但要付出一些数据成本将数据复制到缓存中,然后稍后将其从磁盘复制出来。更糟糕的是,它可能会污染缓存与所有这些数据,因此不再存储其他更有可能受益于缓存的数据。


不要绕过缓冲区。请异步执行IO操作,或者至少在单独的线程上执行。浪费CPU是不好的,但无论您做什么,请保持操作系统缓冲区填充,以便它可以保持驱动器的高效性。 - Eliot Gillum
@EliotGillum:就像这样的评论会让 Stack Overflow 的最佳贡献者们相信他们不如放弃来种花而不是帮助别人。你有很多同行,但你个人对使世界变得更糟负有责任。请重新阅读回答的最后一段。根据需要继续反复阅读,以认识到你的评论是彻头彻尾的错误。 - Jerry Coffin
残忍从来不会让社区变得更好,无论你有多少声望。 - Eliot Gillum
我不认为这是残忍的。我认为这是对事实的观察。"...我的目标是捕获通过千兆以太网传入的数据流,并将其简单地保存到文件中。" 这并没有表明他会从污染缓存中接收到的文件内容中受益。 - Jerry Coffin

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