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


85

我正尝试通过www.spoj.com解决此问题:FCTRL-阶乘

您真的不必阅读它,如果您好奇的话就去做:)

首先,我用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 ++执行结果

但是后来我看到一些评论,声称它们的时间执行少于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执行结果

现在,代码几乎相同,我std::ios_base::sync_with_stdio(false);按照此处的建议将其添加到C ++代码中,以关闭与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";

但是我仍然看到C与C ++代码相比x9的性能更好,内存使用更低。

这是为什么?

编辑

我固定unsigned longunsigned int在C代码。应该是这样unsigned int,上面显示的结果与新(unsigned int)版本有关。


31
C ++流在设计上非常慢。因为缓慢而稳定在比赛中获胜。:P(在我被
解雇

7
缓慢性并非来自安全性或适应性。所有的流标志都过度设计了。
Karoly Horvath

8
@AlexLop。使用astd::ostringstream累加输出并最终std::cout 一次将其发送给所有人,使时间减少到0.02。std::cout在他们的环境中循环使用只会变得很慢,我认为没有简单的方法可以改善它。
高炉炉水

6
没有其他人担心这些计时是使用二氢萘酮获得的事实吗?
ildjarn 2015年

6
@Olaf:恐怕我不同意,对于所有选择的标签,此类问题都非常重要。C和C ++通常足够接近,因此这种性能上的差异可用于解释。我很高兴我们找到了它。因此,也许应该改进GNU libc ++。
chqrlie 2015年

Answers:


56

这两个程序做的完全一样。它们使用相同的精确算法,并且由于其复杂性较低,因此它们的性能主要受输入和输出处理效率的约束。

scanf("%d", &fact_num);一方面扫描输入,另一方面cin >> fact_num;看起来都不是很昂贵。实际上,由于在编译时就知道转换类型,并且C ++编译器可以直接调用正确的解析器,因此在C ++中它的成本应该更低。输出同样如此。您甚至可以编写一个单独的调用点printf("%s","\n");,但是C编译器足以将其编译为对的调用putchar('\n');

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

完全禁用对stdoutC的缓冲会使C实现的速度比C ++版本还要慢。另一个测试由AlexLop与fflush(stdout);最后一个之后printf的产率性能为C ++版本相似。它不像完全禁用缓冲那样慢,因为输出是以小块而不是一次写入一个字节的形式写入系统的。

这似乎指向您的C ++库中的特定行为:我怀疑您的系统的实现,cincout在向cout要求输入时刷新输出到cin。一些C库也可以执行此操作,但是通常仅在对终端进行读写操作时才这样做。www.spoj.com网站完成的基准测试可能会将输入和输出重定向到文件或从文件重定向。

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

Blastfurnace进行的另一项测试将所有输出存储在一个文件中,std::ostringstream并在一次爆炸中进行冲洗,最终将C ++性能提高到基本C版本。QED。

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

PS:您的算法不正确,fact_num >= UINT_MAX / 5因为fives *= 5它将在变为之前溢出并环绕> fact_num。您可以通过纠正此fivesunsigned longunsigned long long如果这些类型之一大于unsigned int。也%u用作scanf格式。您很幸运,www.spoj.com的人员对其基准测试不太严格。

编辑:如后面的vitaux解释,此行为确实由C ++标准强制执行。 默认情况下cin绑定coutcin需要重新填充输入缓冲区的输入操作将导致cout刷新挂起的输出。在OP的实现中,cin似乎cout有系统地刷新,这有点过大且明显无效。

伊利亚·波波夫(Ilya Popov)为此提供了一个简单的解决方案:cin可以cout通过施放除以下以外的其他魔法咒语而不受限制std::ios_base::sync_with_stdio(false);

cin.tie(nullptr);

另请注意,当使用std::endl而不是在'\n'上产生行尾时,也会发生这种强制冲洗cout。将输出线更改为更具C ++惯用性和无辜的外观cout << num_of_trailing_zeros << endl;将以同样的方式降低性能。


2
您可能对流冲洗是正确的。将输出收集到a中std::ostringstream并最终将其全部输出一次,这使时间减少到与C版本相当。
高炉炉水

2
@ DavidC.Rankin:我冒险一个猜想(当阅读cin时,cout被冲洗了),设计了一种方法来证明它,AlexLop实施了它,并且确实提供了令人信服的证据,但是Blastfurnace想出了另一种方法来证明我的观点和他的测试提供同样令人信服的证据。我拿它作为证明,但是,当然,它不是一个完全正式的证明,可以看一下C ++源代码。
chqrlie 2015年

2
我尝试使用ostringstream输出,它给出了时间0.02 QED :)。关于fact_num >= UINT_MAX / 5,好点!
罗伯

1
将所有输入收集到a中vector,然后处理计算(不带ostringstream),将得到相同的结果!时间0.02。二者结合起来vector,并ostringstream没有更多的改进。同一时间0.02
亚历克斯·洛普。

2
即使sizeof(int) == sizeof(long long)是这样,一个更简单的解决方法仍然有效:在循环的主体中添加一个测试,num_of_trailing_zeros += fact_num/fives;以检查是否fives已达到最大值:if (fives > UINT_MAX / 5) break;
chqrlie 2015年

44

iostream同时使用cin和时,使s更快的另一个技巧cout是调用

cin.tie(nullptr);

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

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

在这种情况下,您要确保在开始等待输入之前确实显示了提示。通过上面的界线,您可以打破领带cincout变得独立。

从C ++ 11开始,使用iostream获得更好性能的另一种方法是与std::getline一起使用std::stoi,如下所示:

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

这种方式在性能上可以接近C风格,甚至可以超越scanfgetchar尤其是getchar_unlocked与手写解析一起使用时,仍然可以提供更好的性能。

PS。我写了一篇文章,比较了几种使用C ++输入数字的方法,这对在线法官很有用,但抱歉,它仅以俄语提供。但是,代码示例和最终表应该是可以理解的。


1
感谢您的解释和解决方案的+1,但是您建议的带有std::readlinestd::stoi在功能上不等同于OPs代码的替代方法。双方cin >> x;scanf("%f", &x);接受蚂蚁空格作为分隔符,可以有在同一行多个号码。
chqrlie 2015年

27

问题是,引用cppreference

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

这很容易测试:如果更换

cin >> fact_num;

scanf("%d", &fact_num);

和相同,cin >> num_of_inputs但请注意cout,在C ++版本(或IOStream版本)中,您将获得与C语言几乎相同的性能:

在此处输入图片说明

如果您保留cin但更换,也会发生相同的情况

cout << num_of_trailing_zeros << "\n";

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

一个简单的解决方案是解开coutcin通过伊利亚安德波波夫提到:

cin.tie(nullptr);

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

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


感谢您的解释。但是引用C ++ 14 27.7.2.1.3:类basic_istream :: sentry首先,如果is.tie()不是空指针,则该函数调用is.tie()->flush()以将输出序列与任何关联的外部C流同步。如果的放置区域is.tie()为空,则可以禁止该调用。另外,允许实现将调用推迟到刷新,直到发生调用is.rdbuf()->underflow()。如果在销毁哨兵对象之前没有发生此类调用,则可以完全消除对冲洗的调用。
chqrlie 2015年

和C ++一样,事情比看起来复杂。OP的C ++库效率不如标准所允许的那样。
chqrlie 2015年

感谢您的cppreference链接。我不喜欢在打印屏幕虽然☺了“错误的答案”
亚历罗布泊。

@AlexLop。糟糕,修复了“错误答案”问题=)。忘记更新其他cin(尽管这不会影响时间)。
vitaut 2015年

@chqrlie是的,但是即使在下溢情况下,与stdio解决方案相比,性能也可能会更差。感谢您的标准参考。
vitaut 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.