【问题标题】:How to get IOStream to perform better?如何让 IOStream 表现更好?
【发布时间】:2011-07-07 04:19:09
【问题描述】:

大多数学习 C 的 C++ 用户更喜欢使用 printf / scanf 系列函数,即使他们使用 C++ 进行编码也是如此。

虽然我承认我发现界面方式更好(尤其是类似 POSIX 的格式和本地化),但似乎压倒性的问题是性能。

看看这个问题:

How can I speed up line by line reading of a file

似乎最好的答案是使用 fscanf,而 C++ ifstream 的速度始终慢 2-3 倍。

我认为,如果我们可以编译一个“技巧”存储库来提高 IOStreams 性能,哪些有效,哪些无效。

需要考虑的要点

  • 缓冲 (rdbuf()->pubsetbuf(buffer, size))
  • 同步 (std::ios_base::sync_with_stdio)
  • 语言环境处理(我们可以使用精简的语言环境,还是完全删除它?)

当然,欢迎使用其他方法。

注意:提到了 Dietmar Kuhl 的“新”实现,但我无法找到有关它的许多细节。以前的引用似乎是死链接。

【问题讨论】:

  • 我将此问题作为常见问题解答。如果您认为这是错误的,请随时恢复。
  • @Matthieu:Dietmar 曾经说过他的工作被遗弃了,但我找不到在哪里。 (一般来说,你需要搜索新闻组才能找到这些东西。comp.lang.c++.moderated 是 90 年代所有有趣的 C++ 讨论的地方。)
  • 这个因素是否也适用于 g++?我似乎记得在 gnu stdlib 实现中进行了一些工作,以消除不必要的性能损失。 (我很少做性能敏感的格式化 IO,所以不知道)。
  • @sbi,我很确定他停下来继续工作。这个问题最近在 clc++m 上再次出现,他确实参与了。
  • @AProgrammer 性能差异本质上是一个都市传说,由两个事实提供:(1) c++stdlib 的旧版实现 较慢。 (2)很多人不知道std::ios_base::sync_with_stdio

标签: c++ optimization iostream c++-faq c++-standard-library


【解决方案1】:

这是我目前收集到的:

缓冲

如果默认情况下缓冲区很小,增加缓冲区大小肯定可以提高性能:

  • 它减少了 HDD 命中次数
  • 它减少了系统调用的次数

可以通过访问底层的streambuf 实现来设置缓冲区。

char Buffer[N];

std::ifstream file("file.txt");

file.rdbuf()->pubsetbuf(Buffer, N);
// the pointer reader by rdbuf is guaranteed
// to be non-null after successful constructor

@iavr 的警告:根据cppreference,最好在打开文件之前致电pubsetbuf。不同的标准库实现有不同的行为。

区域设置处理:

Locale 可以在涉及数字或日期的情况下执行字符转换、过滤和更巧妙的技巧。它们经历了一个由动态调度和虚拟调用组成的复杂系统,因此移除它们有助于减少惩罚。

默认的C 语言环境意味着不执行任何转换以及在机器之间保持统一。这是一个很好的默认值。

同步:

我看不到使用此工具的任何性能改进。

可以使用sync_with_stdio 静态函数访问全局 设置(std::ios_base 的静态成员)。

测量:

玩这个,我玩弄了一个简单的程序,在 SUSE 10p3 上使用gcc 3.4.2-O2 编译。

C : 7.76532e+06
C++:1.0874e+07

对于默认代码,这代表大约 20%... 的减速。实际上,篡改缓冲区(在 C 或 C++ 中)或同步参数 (C++) 并没有产生任何改进。

其他人的结果:

@Irfy on g++ 4.7.2-2ubuntu1、-O3、虚拟化 Ubuntu 11.10、3.5.0-25-generic、x86_64、足够的 ram/cpu、196MB 的几个“find / >> largefile.txt”运行

C : 634572 C++:473222

C++ 快 25%

@Matteo Italia 在 g++ 4.4.5、-O3、Ubuntu Linux 10.10 x86_64 上使用随机 180 MB 文件

C : 910390
C++:776016

C++ 快 17%

@Bogatyr 在 g++ i686-apple-darwin10-g++-4.2.1 (GCC) 4.2.1(Apple Inc. build 5664)上,mac mini,4GB ram,除此测试外空闲,数据文件为 168MB

C : 4.34151e+06
C++:9.14476e+06

C++ 慢 111%

@Asu on clang++ 3.8.0-2ubuntu4、Kubuntu 16.04 Linux 4.8-rc3、8GB 内存、i5 Haswell、Crucial SSD、88MB 数据文件(tar.xz 存档)

C : 270895 C++:162799

C++ 快 66%

所以答案是:这是一个实施质量问题,实际上取决于平台:/

此处为对基准测试感兴趣的人提供的完整代码:

#include <fstream>
#include <iostream>
#include <iomanip>

#include <cmath>
#include <cstdio>

#include <sys/time.h>

template <typename Func>
double benchmark(Func f, size_t iterations)
{
  f();

  timeval a, b;
  gettimeofday(&a, 0);
  for (; iterations --> 0;)
  {
    f();
  }
  gettimeofday(&b, 0);
  return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) -
         (a.tv_sec * (unsigned int)1e6 + a.tv_usec);
}


struct CRead
{
  CRead(char const* filename): _filename(filename) {}

  void operator()() {
    FILE* file = fopen(_filename, "r");

    int count = 0;
    while ( fscanf(file,"%s", _buffer) == 1 ) { ++count; }

    fclose(file);
  }

  char const* _filename;
  char _buffer[1024];
};

struct CppRead
{
  CppRead(char const* filename): _filename(filename), _buffer() {}

  enum { BufferSize = 16184 };

  void operator()() {
    std::ifstream file(_filename, std::ifstream::in);

    // comment to remove extended buffer
    file.rdbuf()->pubsetbuf(_buffer, BufferSize);

    int count = 0;
    std::string s;
    while ( file >> s ) { ++count; }
  }

  char const* _filename;
  char _buffer[BufferSize];
};


int main(int argc, char* argv[])
{
  size_t iterations = 1;
  if (argc > 1) { iterations = atoi(argv[1]); }

  char const* oldLocale = setlocale(LC_ALL,"C");
  if (strcmp(oldLocale, "C") != 0) {
    std::cout << "Replaced old locale '" << oldLocale << "' by 'C'\n";
  }

  char const* filename = "largefile.txt";

  CRead cread(filename);
  CppRead cppread(filename);

  // comment to use the default setting
  bool oldSyncSetting = std::ios_base::sync_with_stdio(false);

  double ctime = benchmark(cread, iterations);
  double cpptime = benchmark(cppread, iterations);

  // comment if oldSyncSetting's declaration is commented
  std::ios_base::sync_with_stdio(oldSyncSetting);

  std::cout << "C  : " << ctime << "\n"
               "C++: " << cpptime << "\n";

  return 0;
}

【讨论】:

  • 实际上我发现 C++ 更快(g++ 4.4.5,-O3,Ubuntu Linux 10.10 x86_64):我得到了一个随机的 180 MB 文件C: 910390 C++: 776016
  • @Matteo:那太好了。我也需要尝试 g++4.3.2。
  • 导致这个问题的问题与偏好无关,它与“典型”案例输入处理的具体测量有关。您的基准并不是很有趣,因为它不符合现实世界的情况。相反,您为什么不编写一个 shell 脚本,在一组大文件上通过 1 次迭代运行您的程序,并测量总挂钟时间。
  • @Bogatyr gettimeofdaytime 精确。此外,这一个很好的近似真实世界案例:读取数据。毕竟,我们不想衡量其他的东西,只衡量数据的读取。所以这个基准很好。将两个代码放在同一个可执行文件中也很好。只需确保运行足够的基准测试迭代以抵消预热减速(或在开始时运行一次,Mathieu 会这样做)。这个基准优于您建议的“改进”。
  • 我刚刚在 3 台 linux 机器上进行了测试,使用 g++ 从 4.5.4 编译到 4.7.2,差异从 C++ 快 25% 到 C++ 快 40%。
【解决方案2】:

另外两个改进:

在大量输入/输出之前发出std::cin.tie(nullptr);

引用http://en.cppreference.com/w/cpp/io/cin:

一旦构造了 std::cin,std::cin.tie() 返回 &std::cout,同样,std::wcin.tie() 返回 &std::wcout。这意味着如果有任何字符等待输出,则 std::cin 上的任何格式化输入操作都会强制调用 std::cout.flush()。

您可以通过从std::cout 解开std::cin 来避免刷新缓冲区。这与对std::cinstd::cout 的多次混合调用有关。请注意,调用std::cin.tie(std::nullptr); 会使程序不适合用户以交互方式运行,因为输出可能会延迟。

相关基准:

文件test1.cpp

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  int i;
  while(cin >> i)
    cout << i << '\n';
}

文件test2.cpp

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);
  cin.tie(nullptr);

  int i;
  while(cin >> i)
    cout << i << '\n';

  cout.flush();
}

均由g++ -O2 -std=c++11 编译。编译器版本:g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4(是的,我知道,很老了)。

基准测试结果:

work@mg-K54C ~ $ time ./test1 < test.in > test1.in

real    0m3.140s
user    0m0.581s
sys 0m2.560s
work@mg-K54C ~ $ time ./test2 < test.in > test2.in

real    0m0.234s
user    0m0.234s
sys 0m0.000s

test.in 包含 1179648 行,每行仅包含一个 5。它是 2.4 MB,很抱歉没有在这里发布。)。

我记得解决了一个算法任务,在线法官在没有cin.tie(nullptr) 的情况下一直拒绝我的程序,但用cin.tie(nullptr)printf/scanf 而不是cin/cout 接受它。

使用'\n' 而不是std::endl

引用http://en.cppreference.com/w/cpp/io/manip/endl

在输出序列 os 中插入一个换行符并刷新它,就像调用 os.put(os.widen('\n')) 后跟 os.flush() 一样。

您可以通过打印'\n' 而不是endl 来避免刷新缓冲区。

相关基准:

文件test1.cpp

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  for(int i = 0; i < 1179648; ++i)
    cout << i << endl;
}

文件test2.cpp

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  for(int i = 0; i < 1179648; ++i)
    cout << i << '\n';
}

如上编译。

基准测试结果:

work@mg-K54C ~ $ time ./test1 > test1.in

real    0m2.946s
user    0m0.404s
sys 0m2.543s
work@mg-K54C ~ $ time ./test2 > test2.in

real    0m0.156s
user    0m0.135s
sys 0m0.020s

【讨论】:

  • 啊,是的,endl 的情况通常为爱好者所熟知,但很多教程默认使用它(为什么????)它经常让初学者/中级程序员绊倒。至于tie:我今天学习了!我知道提示用户会强制刷新,但不知道它是如何控制的。
【解决方案3】:

有趣的是,您说 C 程序员在编写 C++ 时更喜欢 printf,因为我看到很多 C 代码,而不是使用 coutiostream 来编写输出。

直接使用filebuf 通常可以获得更好的性能(Scott Meyers 在 Effective STL 中提到了这一点),但是直接使用 filebuf 的文档相对较少,大多数开发人员更喜欢std::getline,这在大多数情况下更简单。

关于语言环境,如果您创建分面,通常会通过一次创建一个包含所有分面的语言环境、将其存储并将其注入您使用的每个流中来获得更好的性能。

我最近确实在这里看到了另一个主题,所以这几乎是重复的。

【讨论】:

  • 如果您通过直接使用文件缓冲区获得更好的性能,那么这意味着解析代码(无论如何用于读取)才是性能消耗,因为这是 std::istream 包装缓冲区的内容。不幸的是,广泛的 IO 流实现在底层使用 printf()/scanf(),这肯定比直接使用 C std lib IO 慢。 (另请参阅我对@Konrad 的评论。)
  • “除了使用 cout 和 iostream 之外的 C 代码”——我们称其为“带有 iostreams 的 C”,这是许多大学课程中 C++ 的通行证。
猜你喜欢
  • 2015-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多