【问题标题】:C++ iostream vs. C stdio performance/overheadC++ iostream 与 C stdio 性能/开销
【发布时间】:2016-06-18 07:04:50
【问题描述】:

我正在尝试了解如何提高此 C++ 代码的性能,使其与它所基于的 C 代码相提并论。 C 代码如下所示:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef struct point {
  double x, y;
} point_t;

int read_point(FILE *fp, point_t *p) {
  char buf[1024];
  if (fgets(buf, 1024, fp)) {
    char *s = strtok(buf, " ");
    if (s) p->x = atof(s); else return 0;
    s = strtok(buf, " ");
    if (s) p->y = atof(s); else return 0;
  }
  else
    return 0;
  return 1;
}

int main() {
  point_t p;
  FILE *fp = fopen("biginput.txt", "r");

  int i = 0;
  while (read_point(fp, &p))
    i++;

  printf("read %d points\n", i);
  return 0;
}

C++ 代码如下所示:

#include <iostream>
#include <fstream>

using namespace std;

struct point {
  double x, y;
};

istream &operator>>(istream &in, point &p) {
  return in >> p.x >> p.y;
}

int main() {
  point p;
  ifstream input("biginput.txt");

  int i = 0;
  while (input >> p)
    i++;

  cout << "read " << i << " points" << endl;
  return 0;
}

我喜欢 C++ 代码更短更直接,但是当我在我的机器上同时运行它们时,我会得到非常不同的性能(两者都在同一台机器上针对 138 MB 的测试文件运行):

$ time ./test-c
read 10523988 points
    1.73 real         1.68 user         0.04 sys
# subsequent runs:
    1.69 real         1.64 user         0.04 sys
    1.72 real         1.67 user         0.04 sys
    1.69 real         1.65 user         0.04 sys

$ time ./test-cpp
read 10523988 points
   14.50 real        14.36 user         0.07 sys
# subsequent runs
   14.79 real        14.43 user         0.12 sys
   14.76 real        14.40 user         0.11 sys
   14.58 real        14.36 user         0.09 sys
   14.67 real        14.40 user         0.10 sys

连续多次运行任一程序并不会改变 C++ 版本大约慢 10 倍的结果。

文件格式只是以空格分隔的双精度行,例如:

587.96 600.12
430.44 628.09
848.77 468.48
854.61 76.18
240.64 409.32
428.23 643.30
839.62 568.58

有减少我遗漏的开销的技巧吗?

编辑 1:使操作符内联似乎有一个很小但可能可以检测到的效果:

   14.62 real        14.47 user         0.07 sys
   14.54 real        14.39 user         0.07 sys
   14.58 real        14.43 user         0.07 sys
   14.63 real        14.45 user         0.08 sys
   14.54 real        14.32 user         0.09 sys

这并不能真正解决问题。

编辑 2:我正在使用 clang:

$ clang --version
Apple LLVM version 7.0.0 (clang-700.0.72)
Target: x86_64-apple-darwin15.5.0
Thread model: posix

我没有在 C 或 C++ 上使用任何优化级别,并且它们都在我的 Mac 上使用相同版本的 Clang 进行编译。可能是 OS X 10.11 上 Xcode (/usr/bin/clang) 附带的版本。我认为如果我在其中一个而不是另一个中启用优化,或者使用不同的编译器,这会使问题变得模糊。

编辑 3:将 istream &amp;operator&gt;&gt; 替换为其他内容

我已经重写了 istream 运算符以更接近 C 版本,并且它得到了改进,但我仍然看到大约 5 倍的性能差距。

inline istream &operator>>(istream &in, point &p) {
  string line;
  getline(in, line);

  if (line.empty())
    return in;

  size_t next = 0;
  p.x = stod(line, &next);
  p.y = stod(line.substr(next));
  return in;
}

运行:

$ time ./test-cpp
read 10523988 points
    6.85 real         6.74 user         0.05 sys
# subsequently
    6.70 real         6.62 user         0.05 sys
    7.16 real         6.86 user         0.12 sys
    6.80 real         6.59 user         0.09 sys
    6.79 real         6.59 user         0.08 sys

有趣的是,用-O3 编译它是一个很大的改进:

$ time ./test-cpp
read 10523988 points
    2.44 real         2.38 user         0.04 sys
    2.43 real         2.38 user         0.04 sys
    2.49 real         2.41 user         0.04 sys
    2.51 real         2.42 user         0.05 sys
    2.47 real         2.40 user         0.05 sys

编辑 4:用 C 的东西替换 istream 操作符的主体>>

这个版本的性能非常接近 C:

inline istream &operator>>(istream &in, point &p) {
  char buf[1024];
  in.getline(buf, 1024);
  char *s = strtok(buf, " ");
  if (s)
    p.x = atof(s);
  else
    return in;

  s = strtok(NULL, " ");
  if (s)
    p.y = atof(s);

  return in;
}

未优化的计时让我们进入 2 秒的领域,优化将其置于未优化的 C 之上(尽管优化的 C 仍然获胜)。准确地说,没有优化:

    2.13 real         2.08 user         0.04 sys
    2.14 real         2.07 user         0.04 sys
    2.33 real         2.15 user         0.05 sys
    2.16 real         2.10 user         0.04 sys
    2.18 real         2.12 user         0.04 sys
    2.33 real         2.17 user         0.06 sys

与:

    1.16 real         1.10 user         0.04 sys
    1.19 real         1.13 user         0.04 sys
    1.11 real         1.06 user         0.03 sys
    1.15 real         1.09 user         0.04 sys
    1.14 real         1.09 user         0.04 sys

具有优化功能的 C,只是为了实现对苹果的需求:

    0.81 real         0.77 user         0.03 sys
    0.82 real         0.78 user         0.04 sys
    0.87 real         0.80 user         0.04 sys
    0.84 real         0.77 user         0.04 sys
    0.83 real         0.78 user         0.04 sys
    0.83 real         0.77 user         0.04 sys

我想我可以接受这个,但作为一个 C++ 新手,我现在想知道:

  1. 是否值得尝试以另一种方式执行此操作?我不确定 istream 运算符内部发生的事情是否重要>>。
  2. 除了这三种方法之外,还有其他方法可以构建性能更好的 C++ 代码吗?
  3. 这是惯用语吗?如果不是,大多数人是否只是接受表演的本来面目?

编辑 5:这个问题与关于 printf 的答案完全不同,我看不出链接的问题这应该是如何解决正上方三个点中的任何一个问题。

【问题讨论】:

  • 这些例子不是比较喜欢的。 C++ 版本从流对象中读取每个值,并且可以与fscanf("%f &amp;f ", &amp;p-&gt;x, &amp;p-&gt;y) 之类的使用相媲美。 C 版本是手工制作的,通过直接读取一行然后解析该行来击败这种fscanf() 调用的性能。 C++ 版本可以类似地设计为使用std::getline()std::istream::getline(),然后解析字符串。
  • @Peter 我认为没有人在实践中使用 fscanf;它非常脆弱。但我很高兴做出建议的更改以使用 getline — 之后下一步是什么,创建一个字符串流并从中使用 &gt;&gt; 而不是 iostream?
  • @Peter 请注意,我并不是想在这里创建一个完美的基准,实际上,作为 C++ 的新手,我对在实践中加速一些 C++ 代码很感兴趣。
  • 我在评论您的代码所比较的内容 - 不提倡使用或避免使用 fscanf()。但是,您拥有的 C++ 代码在功能上等同于 fscanf()(除了需要解析格式字符串),尽管不那么脆弱。读取后解析字符串的一种方法是使用std::istrstream。还有其他的。
  • 抱歉,比较未优化二进制文件的时间是毫无意义的。根据经验,c++ 函数通常有更多的间接层,如果您激活优化,编译器会完全优化这些层。

标签: c++ performance iostream


【解决方案1】:

导致性能显着差异的原因是整体功能的显着差异。

我会尽力详细比较你这两种看似等效的方法。

在 C 中:

循环

  • 读取字符直到检测到换行符或文件结尾或达到最大长度 (1024)
  • 标记化寻找硬编码的空白分隔符
  • 不带任何问题解析成双精度

在 C++ 中:

循环

  • 读取字符直到检测到默认分隔符之一。这并不限制检测到您的实际数据模式。以防万一,它将检查更多分隔符。无处不在。
  • 一旦找到分隔符,它将尝试优雅地解析累积的字符串。它不会在您的数据中假设模式。例如,如果有 800 个连续的数字字符并且不再适合该类型,则它必须能够自行检测到这种可能性,因此会为此增加一些开销。

我建议的一种提高性能的方法与 Peter 在上述 cmets 中所说的非常接近。在operator&gt;&gt; 中使用getline,这样您就可以了解您的数据。像这样的东西应该能够让你恢复一些速度,认为它有点像 C-ing 你的代码的一部分:

istream &operator>>(istream &in, point &p) {
    char bufX[10], bufY[10];
    in.getline(bufX, sizeof(bufX), ' ');
    in.getline(bufY, sizeof(bufY), '\n');
    p.x = atof(bufX);
    p.y = atof(bufY);
    return in;
}

希望对你有帮助。

编辑:应用了 nneonneo 的评论

【讨论】:

  • 尼特:你想要in.getline(bufX, sizeof(bufX), ' ');。也使用\n 表示bufY
  • @nneonneo 感谢您的输入,我已相应更新
  • 每个人都对我大加赞赏,好像我在尝试对 iostreams 进行基准测试,或者好像我说过这两个程序是等价的,但我从来没有这么说,只是我有一个 C 程序和试图将其与 C++ 程序相匹配。我接受并支持你,因为你的代码确实有效,而且你没有给我太多不相关的建议。就好像您真的阅读了我的问题一样。但这整个经历非常消极,我不知道该怎么办。
  • @DanielLyons 您的问题很有趣,在回答之前我对其进行了深入研究。我的总体建议是:采用更易于使用和维护的方法。在这方面,C++ 方法通常更灵活并且可以更好地扩展,而 C 通常是一头野兽,可以在一个确切的时刻适应您的确切数据结构。每当出现性能问题时,回到 C 中的一些邪恶机制,并将它们包装成有意义的函数名称。
  • 这是个好建议!我会记住的。我很欣赏它的灵活性,我只是有一个问题(至少在文件 I/O 方面)如果没有“一些邪恶的机制”,它可能太贵了。
【解决方案2】:

更新:我做了更多的测试,并且(如果你有足够的内存)有一个非常简单的解决方案——至少在我的 VS2015 机器上——优于 c 解决方案:只需缓冲字符串流中的文件。

ifstream input("biginput.txt");
std::stringstream buffer;
buffer << input.rdbuf();
point p;
while (buffer >> p) {
    i++
}

所以问题似乎与 c++ 流机制本身并没有太大关系,而特别是与 ifstream 的内部结构有关。


这是我原来的(过时的)答案: @Frederik 已经解释说,性能不匹配(至少部分)与功能差异有关。

关于如何恢复性能:在我的 VS2015 机器上,以下运行时间大约是 C 解决方案要求的 2/3(尽管在我的机器上,两者之间“仅”有 3 倍的性能差距您的原始版本开始):

istream &operator >> (istream &in, point &p) {
    thread_local std::stringstream ss;
    thread_local std::string s;

    if (std::getline(in, s)) {
        ss.str(s);
        ss >> p.x >> p.y;
    }
    return in;
}

我对 thread_local 变量不太满意,但它们对于消除重复动态内存分配的开销是必要的。

【讨论】:

  • 感谢您在这里尝试提供帮助(我特别感谢您在上面的 cmets 中提供的帮助),但这对我来说就像是一堆堆黑客攻击。不过,我很惊讶地听到 Windows 上的差距如此之小。投票是因为您实际上是在尝试回答我的问题。
  • 我最喜欢这个答案,似乎更直接地解决了这个问题。我想建议通过使用 open/read/close 而不是 fopen/fgets/fclose 来“禁用”C 程序上的缓存,看看效果如何。
【解决方案3】:

如 cmets 中所述,确保读取输入的实际算法在 C++ 中与在 C 中一样好。并确保您有 std::ios::sync_with_stdio(假) 因此 iostream 不会因与 C stdio 同步而减慢。

但根据我的经验,C stdio 比 C++ iostreams 快,但 C lib 不是类型安全和可扩展的。

【讨论】:

  • std::ios::sync_with_stdio 仅影响标准输入/输出流(如std::cout/stdout
  • 是的,没错。了解sync_with_stdio很好,但如果程序永远不会被修改为从stdin读取,则无需考虑同步。
  • 这不是我实际提出的问题的答案。
  • 是的。使用与 C 中相当好的算法,即使这样,您也应该期望旧的、简单的 C 标准输出更快。
猜你喜欢
  • 2013-06-21
  • 2019-03-18
  • 2021-12-29
  • 2013-08-18
  • 1970-01-01
  • 1970-01-01
  • 2020-08-17
  • 2011-05-05
  • 2016-12-19
相关资源
最近更新 更多