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

时间:2015-12-06 20:16:05

标签: c++ c performance gcc iostream

我试图从www.spoj.com解决此练习:FCTRL - Factorial

你真的不必阅读它,只要你好奇就去做吧。)

首先我在 C ++ 中实现了它(这是我的解决方案):

#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

的解决方案

结果是:时间 0.18 Mem 3.3M C++ execution results

但后来我看到一些评论声称他们的执行时间不到0.1。由于我无法考虑更快的算法,我尝试在 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

的解决方案

这次结果是:时间 0.02 Mem 2.1M C execution results

现在代码几乎相同,我将std::ios_base::sync_with_stdio(false);添加到C ++代码中,如同建议here一样,以关闭与C库的stdio缓冲区的同步。我还将printf("%d\n", num_of_trailing_zeros);拆分为printf("%d", num_of_trailing_zeros); printf("%s","\n");以补偿operator<<cout << num_of_trailing_zeros << "\n";的双重调用。

但我仍然看到 x9更好的性能并降低C与C ++代码中的内存使用量。

为什么?

修改

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

3 个答案:

答案 0 :(得分:56)

两个程序完全相同。它们使用相同的精确算法,并且由于其低复杂性,它们的性能主要受输入和输出处理效率的限制。

在一侧用scanf("%d", &fact_num);扫描输入,在另一侧用cin >> fact_num;扫描输入,无论如何都不是很昂贵。事实上,它在C ++中的成本应该更低,因为转换类型在编译时是已知的,并且正确的解析器可以由C ++编译器直接调用。输出也是如此。你甚至要为printf("%s","\n");编写单独的调用,但C编译器足以将其编译为对putchar('\n');的调用。

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

完全禁用stdout的缓冲会使C实现速度慢于C ++版本。 AlexLop在最后fflush(stdout);之后使用printf进行的另一项测试产生与C ++版本类似的性能。它没有完全禁用缓冲那么慢,因为输出以小块而不是一次一个字节写入系统。

这似乎指向C ++库中的特定行为:我怀疑当{{1}请求输入时,系统的cincout实现会将输出刷新到cout }}。一些C库也这样做,但通常只在读/写终端时才这样做。 www.spoj.com网站完成的基准测试可能会重定向文件的输入和输出。

AlexLop做了另一个测试:在向量中一次读取所有输入,然后计算和写入所有输出有助于理解为什么C ++版本要慢得多。它将性能提升到C版本的性能,这证明了我的观点并消除了对C ++格式代码的怀疑。

Blastfurnace的另一项测试,将所有输出存储在一个cin中,并在最后一次冲洗中,确实将C ++性能提高到基本C版本的性能。 QED。

  

std::ostringstream和输出到cin的隔行输入似乎导致非常低效的I / O处理,从而破坏了流缓冲方案。将性能降低10倍。

PS:cout的算法不正确,因为fact_num >= UINT_MAX / 5会溢出并在变为fives *= 5之前回绕。如果其中一种类型大于> fact_num,则可以fives unsigned longunsigned long long来更正此问题。同时使用unsigned int作为%u格式。你很幸运www.spoj.com的人在他们的基准测试中并不是太严格。

编辑:正如后来的vitaux所解释的那样,这种行为确实是由C ++标准强制执行的。默认情况下,scanfcin绑定。来自cout的输入缓冲区需要重新填充的输入操作将导致cin刷新挂起的输出。在OP的实现中,cout似乎系统地刷新cin,这有点矫枉过正,效率明显低。

伊利亚·波波夫为此提供了一个简单的解决方案:除了cout之外,还可以通过施放另一个魔法咒语来解除cin cout

  

std::ios_base::sync_with_stdio(false);

另请注意,使用cin.tie(nullptr);代替std::endl'\n'上生成行尾时,也会发生强制同花。将输出行更改为更多C ++惯用且无辜的cout会以相同的方式降低性能。

答案 1 :(得分:44)

同时使用iostreamcin时,让cout更快的另一个技巧是调用

cin.tie(nullptr);

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

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

在这种情况下,您需要确保在开始等待输入之前实际显示提示。通过上面的一行,您可以打破这种平局,cincout变得独立。

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

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

这种方式可以接近C风格的性能,甚至超过scanf。使用getchar,特别是getchar_unlocked和手写解析仍可提供更好的性能。

PS。我写了a post比较了几种用C ++输入数字的方法,这对于在线评委很有用,但是只有俄语,对不起。但是,代码示例和最终表格应该是可以理解的。

答案 2 :(得分:27)

问题是,引用cppreference

  

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

这很容易测试:如果你替换

cin >> fact_num;

scanf("%d", &fact_num);

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

enter image description here

如果您保留cin但替换

,也会发生同样的情况
cout << num_of_trailing_zeros << "\n";

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

一个简单的解决方案是解开Ilya Popov提到的coutcin

cin.tie(nullptr);

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

  

类basic_istream :: sentry:首先,如果is.tie()不是空指针,则函数调用is.tie() - &gt; flush()以使输出序列与任何关联的外部C流同步。除非is.tie()的put区域为空,否则可以抑制此调用。此外,允许实现将调用推迟到刷新,直到发生is.rdbuf() - &gt; underflow()的调用。如果在销毁岗哨对象之前没有发生此类调用,则可以完全取消对flush的调用。