C++ iostream 与 C stdio performance/overhead

C++ iostream vs. C stdio performance/overhead

我正在尝试了解如何提高此 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倍左右的结果。

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

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 进行编译。可能是 Xcode (/usr/bin/clang) 在 OS X 10.11 上附带的版本。我认为如果我在一个中启用优化而不在另一个中启用优化或使用不同的编译器,这会使问题变得模糊。

编辑 3:用其他内容替换 istream &operator>>

我重写了 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 的答案完全不同,我看不出链接问题 this is supposedly a duplicate of addresses 这三个点中的任何一个正上方。

造成性能显着差异的是整体功能的显着差异。

我会尽最大努力详细比较你们这两种看似相同的方法。

C:

循环播放

  • 读取字符直到检测到换行符或文件结尾或达到最大长度 (1024)
  • Tokenize 寻找硬编码的白色-space 定界符
  • 毫无疑问解析成double

在 C++ 中:

循环播放

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

我建议的一种提高性能的方法与 Peter 在上述评论中所说的非常接近。在 operator>> 中使用 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 的评论

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

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

更新: 我做了一些更多的测试(如果你有足够的内存)有一个非常简单的解决方案 - 至少在我的 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(尽管在我的机器上,有 "only"原始版本之间的 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 变量不太满意,但它们对于消除重复动态内存分配的开销是必要的。