如何让IOStream表现更好?

时间:2011-03-02 10:39:19

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

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

虽然我承认我发现接口方式更好(特别是类似POSIX的格式和本地化),但似乎压倒性的是性能。

看看这个问题:

  

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

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

我认为如果我们可以编译一个“提示”存储库来提高IOStream的性能,什么可行,什么不可行,那将会很棒。

要考虑的要点

  • 缓冲(rdbuf()->pubsetbuf(buffer, size)
  • 同步(std::ios_base::sync_with_stdio
  • 区域设置处理(我们可以使用修剪后的区域设置,还是完全删除它?)

当然,欢迎其他方法。

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

3 个答案:

答案 0 :(得分:47)

以下是我迄今收集的内容:

<强>缓冲

如果默认情况下缓冲区非常小,增加缓冲区大小肯定会提高性能:

  • 它减少了硬盘命中数
  • 减少了系统调用次数

可以通过访问基础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语言环境意味着不执行任何转换以及跨机器统一。这是一个很好的默认使用。

<强>同步

使用此工具我无法看到任何性能提升。

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

<强>测量

玩这个,我玩了一个简单的程序,使用gcc 3.4.2在SUSE 10p3上使用-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 /&gt;&gt; largefile.txt”运行

     C:634572   C ++:473222

C ++ 快25%

  

@Matteo Italia on g ++ 4.4.5,-O3,Ubuntu Linux 10.10 x86_64,随机180 MB文件

     C:910390
  C ++:776016

C ++ 快17%

  

@Bogatyr on 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 ram,i5 Haswell,Crucial SSD,88MB datafile(tar.xz archive)

     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;
}

答案 1 :(得分:15)

另外两项改进:

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

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

  

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

您可以通过从std::cin解除std::cout来清除缓冲区。这与多个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而不是{{1} }} / cin

使用cout代替'\n'

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

  

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

您可以通过打印std::endl而不是'\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';
}

答案 2 :(得分:1)

有趣的是你说C程序员在编写C ++时更喜欢printf,因为除了使用coutiostream来编写输出之外,我看到很多代码都是C语言。

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

关于区域设置,如果您创建构面,通常会使用所有构面创建一次区域设置,保存它并将其添加到您使用的每个流中,从而获得更好的性能。

我最近在这里看到了另一个关于此问题的话题,所以这几乎是重复的。