在C和C ++中几乎相同的代码之间的执行时间差异很大(x9)

我试图从www.spoj.com解决这个练习: FCTRL – Factorial

你真的不需要阅读,只要你好奇:)

首先我用C ++实现它(这里是我的解决scheme):

#include <iostream> using namespace std; int main() { unsigned int num_of_inputs; unsigned int fact_num; unsigned int num_of_trailing_zeros; std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library's stdio buffers (from https://stackoverflow.com/a/22225421/5218277) cin >> num_of_inputs; while (num_of_inputs--) { cin >> fact_num; num_of_trailing_zeros = 0; for (unsigned int fives = 5; fives <= fact_num; fives *= 5) num_of_trailing_zeros += fact_num/fives; cout << num_of_trailing_zeros << "\n"; } return 0; } 

我上传了它作为g ++ 5.1的解决scheme

结果是: 时间 0.18 Mem 3.3M C ++执行结果

但后来我看到一些评论说他们的执行时间不到0.1。 由于我无法考虑更快的algorithm,我试图在C :中实现相同的代码:

 #include <stdio.h> int main() { unsigned int num_of_inputs; unsigned int fact_num; unsigned int num_of_trailing_zeros; scanf("%d", &num_of_inputs); while (num_of_inputs--) { scanf("%d", &fact_num); num_of_trailing_zeros = 0; for (unsigned int fives = 5; fives <= fact_num; fives *= 5) num_of_trailing_zeros += fact_num/fives; printf("%d", num_of_trailing_zeros); printf("%s","\n"); } return 0; } 

我上传了它作为gcc 5.1的解决scheme

这一次的结果是: 时间 0.02 Mem 2.1M C执行结果

现在代码几乎相同 ,我添加了std::ios_base::sync_with_stdio(false); 到C ++代码,正如在这里所build议的,closures与C库的stdio缓冲区的同步。 我也分裂了printf("%d\n", num_of_trailing_zeros);printf("%d", num_of_trailing_zeros); printf("%s","\n"); printf("%d", num_of_trailing_zeros); printf("%s","\n"); 以补偿operator<<cout << num_of_trailing_zeros << "\n";双重调用cout << num_of_trailing_zeros << "\n";

但是在C和C ++代码中,我仍然看到了x9更好的性能和更低的内存使用率。

这是为什么?

编辑

我在C代码中修改了unsigned long unsigned int 。 它应该是unsigned int ,上面显示的结果与新的( unsigned int )版本有关。

两个程序都完全一样。 它们使用相同的精确algorithm,并且由于其复杂度低,其性能主要受input输出处理效率的限制。

使用scanf("%d", &fact_num);扫描inputscanf("%d", &fact_num); 一方和cin >> fact_num; 另一方面似乎并不昂贵。 事实上,在C ++中它的成本应该更低,因为转换types在编译时是已知的,正确的parsing器可以由C ++编译器直接调用。 输出也一样。 你甚至要写一个单独的printf("%s","\n");调用printf("%s","\n"); ,但是C编译器足够好,可以编译为putchar('\n');

因此,考虑到I / O和计算的复杂性,C ++版本应该比C版本更快。

完全禁用stdout的缓冲会降低C实现的速度,甚至比C ++版本更慢。 AlexLop用fflush(stdout);另一个testingfflush(stdout); 在最后的printf产生与C ++版本相似的性能之后。 它不像完全禁用缓冲那么慢,因为输出一次以小块而不是一个字节写入系统。

这似乎指向您的C ++库中的特定行为:我怀疑当从cin请求input时,系统的cincout将输出刷新到cout 。 一些C库也是这样做的,但通常只有在读写terminal的时候。 http://www.spoj.com网站所做的基准testing可能会redirectinput和输出文件。;

AlexLop做了另一个testing:在vector中一次读取所有的input,然后计算和编写所有的输出,这有助于理解为什么C ++版本慢得多。 它提高了C版本的性能,这certificate了我的观点,并消除了对C ++格式化代码的怀疑。

Blastfurnace的另一个testing,将所有输出存储在std::ostringstream并在最后一次冲洗时确实将C ++的性能提高到基本C版本的性能。 QED。

来自cin交错input和输出到cout似乎导致非常低效的I / O处理,从而破坏了stream缓冲scheme。 性能降低10倍。

PS:你的algorithm对于fact_num >= UINT_MAX / 5是不正确的,因为fives *= 5会溢出并且在它变成> fact_num之前> fact_num 。 如果这些types之一大于unsigned int则可以通过使五为unsigned longunsigned long long来纠正。 同样使用%u作为scanf格式。 你很幸运,在www.spoj.com的人不是太严格的基准。

编辑:如后面解释由vitaux,这种行为确实由C ++标准强制。 cin默认绑定到cout 。 input缓冲区需要重新填充的cin的input操作将导致cout刷新等待输出。 在OP的实现中, cin似乎系统地刷新了cout ,这有点矫枉过正,显然效率低下。

Ilya Popov为此提供了一个简单的解决scheme:除了std::ios_base::sync_with_stdio(false);之外, cin还可以通过施放另一个魔法咒语从cout解开std::ios_base::sync_with_stdio(false);

cin.tie(nullptr);

另外请注意,当使用std::endl而不是'\n'cout上产生行尾时,也会发生这样的强制刷新。 将输出行更改为更多的C ++习惯和无辜的看起来cout << num_of_trailing_zeros << endl; 会以相同的方式降低性能。

使用cincout时使iostream更快的另一个技巧是调用

 cin.tie(nullptr); 

默认情况下,当你从cininput任何内容时,它会刷新cout 。 如果您执行交错input和输出,它可能会严重损害性能。 这是为命令行界面使用完成的,在这里显示一些提示,然后等待数据:

 std::string name; cout << "Enter your name:"; cin >> name; 

在这种情况下,您要确保在开始等待input之前实际显示提示。 随着上面的线你打破领带, cincout成为独立。

从C ++ 11开始,使用iostreams实现更好性能的另一种方法是将std::getlinestd::stoi一起使用,如下所示:

 std::string line; for (int i = 0; i < n && std::getline(std::cin, line); ++i) { int x = std::stoi(line); } 

这种方式在性能上可以接近C-style,甚至超过scanf 。 使用getchar特别是getchar_unlocked与手写parsing仍然可以提供更好的性能。

PS。 我已经写了一篇文章,比较几种在C ++中input数字的方法,对于在线评委很有用,但是只有在俄罗斯,对不起。 代码示例和决赛桌应该是可以理解的。

问题是,引用cppreference :

来自std :: cin的任何input,输出到std :: cerr或者程序终止都强制调用std :: cout.flush()

这很容易testing:如果你更换

 cin >> fact_num; 

 scanf("%d", &fact_num); 

cin >> num_of_inputs一样,但是保持cout你将在C ++版本(或者说IOStream版本)中获得和C中一样的性能:

在这里输入图像说明

如果你保留cin但是replace,也会发生同样的情况

 cout << num_of_trailing_zeros << "\n"; 

 printf("%d", num_of_trailing_zeros); printf("%s","\n"); 

一个简单的解决scheme是解开伊利亚波波夫提到的coutcin

 cin.tie(nullptr); 

标准库实现允许在某些情况下省略调用刷新,但并不总是如此。 这是C ++ 14 27.7.2.1.3的引用(感谢chqrlie):

class basic_istream :: sentry:首先,如果is.tie()不是空指针,则函数调用is.tie() – > flush()来将输出序列与任何关联的外部Cstream同步。 除了is.tie()的放置区域是空的,这个调用可以被抑制。 进一步的实现允许延迟调用flush,直到调用is.rdbuf() – > underflow()发生。 如果在哨兵对象被销毁之前没有发生这样的呼叫,则可以完全消除冲洗的呼叫。