64

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

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

看看这个问题:

如何加快逐行读取文件的速度

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

我认为如果我们可以编译一个“技巧”存储库来提高 IOStreams 的性能,那会很棒,什么有效,什么无效。

需要考虑的要点

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

当然,也欢迎其他方法。

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

4

3 回答 3

50

这是我到目前为止收集的内容:

缓冲

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

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

可以通过访问底层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 在 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 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 数据文件(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;
}
于 2011-03-02T13:52:01.677 回答
19

还有两个改进:

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::cin您可以通过解绑来避免刷新缓冲区std::coutstd::cin这与对和的多次混合调用有关std::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)or 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
于 2016-02-11T13:29:05.387 回答
1

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

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

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

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

于 2011-03-02T11:29:52.683 回答