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

Big difference (x9) in the execution time between almost identical code in C and C++

提问人:Alex Lop. 提问时间:12/7/2015 最后编辑:CommunityAlex Lop. 更新时间:12/7/2015 访问量:6411

问:

我试图从 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.3MC++ 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.1MC execution results

现在代码几乎相同,我按照此处的建议添加到 C++ 代码中,以关闭与 C 库的 stdio 缓冲区的同步。我还拆分了 to 以补偿 in 的双重调用。std::ios_base::sync_with_stdio(false);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 longunsigned intunsigned intunsigned int

C++ C 性能 GCC IOSream

评论

32赞 Mysticial 12/7/2015
C++ 流在设计上非常慢。因为缓慢而稳定地赢得比赛。:P(在我被点燃之前跑)
7赞 Karoly Horvath 12/7/2015
缓慢不是来自安全性或适应性。它对所有流标志都进行了过度设计。
8赞 Blastfurnace 12/7/2015
@AlexLop。使用 a 累加输出并在最后一次性将其发送到所有输出,将时间缩短到 0.02。在他们的环境中,在循环中使用只是速度较慢,我认为没有一种简单的方法可以改进它。std::ostringstreamstd::coutstd::cout
6赞 ildjarn 12/7/2015
难道没有其他人关心这些时间是使用 ideone 获得的吗?
6赞 chqrlie 12/7/2015
@Olaf:恐怕我不同意,这种问题对于所有选择的标签来说都是非常相关的。C 和 C++ 通常非常接近,以至于这种性能差异需要解释。我很高兴我们找到了它。也许 GNU libc++ 应该因此而改进。

答:

57赞 chqrlie 12/7/2015 #1

这两个程序执行完全相同的操作。它们使用相同的精确算法,并且由于其低复杂性,它们的性能主要取决于输入和输出处理的效率。

无论哪种方式,用一侧和另一侧扫描输入似乎都不是很昂贵。事实上,在 C++ 中,它应该成本更低,因为转换类型在编译时是已知的,并且 C++ 编译器可以直接调用正确的解析器。这同样适用于输出。你甚至会写一个单独的调用,但 C 编译器已经足够好了,可以将其编译为对 的调用。scanf("%d", &fact_num);cin >> fact_num;printf("%s","\n");putchar('\n');

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

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

这似乎指向您的 C++ 库中的特定行为:我怀疑您的系统实现了输出,并在请求输入时将输出刷新到 。一些 C 库也这样做,但通常仅在读取/写入终端时。www.spoj.com 站点完成的基准测试可能会将输入和输出重定向到文件或从文件重定向。cincoutcoutcin

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

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

交错输入和输出似乎会导致 I/O 处理效率非常低下,从而破坏了流缓冲方案。性能降低 10 倍。cincout

PS:您的算法不正确,因为在它变成之前会溢出并环绕。您可以通过使 或 如果这些类型之一大于 .也用作格式。你很幸运,www.spoj.com 的家伙在他们的基准上并不太严格。fact_num >= UINT_MAX / 5fives *= 5> fact_numfivesunsigned longunsigned long longunsigned int%uscanf

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

伊利亚·波波夫(Ilya Popov)为此提供了一个简单的解决方案:除了以下方法外,还可以通过施放另一个魔法咒语来解开束缚:cincoutstd::ios_base::sync_with_stdio(false);

cin.tie(nullptr);

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

评论

2赞 Blastfurnace 12/7/2015
您可能对流冲洗的看法是正确的。将输出收集到 a 中并在最后输出一次,可以将时间缩短到与 C 版本相同的水平。std::ostringstream
2赞 chqrlie 12/7/2015
@ DavidC.Rankin:我冒昧地提出了一个猜想(cout 在阅读 cin 时脸红了),设计了一种方法来证明它,AlexLop 实现了它,它确实提供了令人信服的证据,但 Blastfurnace 想出了一种不同的方法来证明我的观点,他的测试给出了同样令人信服的证据。我把它当作证明,但当然它不是一个完全正式的证明,看看C++源代码就可以了。
2赞 Alex Lop. 12/7/2015
我尝试使用输出,它给出了时间 0.02 QED :)。关于,好点!ostringstreamfact_num >= UINT_MAX / 5
1赞 Alex Lop. 12/7/2015
将所有输入收集到 a 中,然后处理计算(不使用 )会得到相同的结果!时间 0.02。将两者结合起来,并不能进一步改善它。同时 0.02vectorostringstreamvectorostringstream
2赞 chqrlie 12/7/2015
一个更简单的修复方法,即使这样也可以:在循环的主体中添加一个测试,以检查是否已达到最大值:sizeof(int) == sizeof(long long)num_of_trailing_zeros += fact_num/fives;fivesif (fives > UINT_MAX / 5) break;
27赞 vitaut 12/7/2015 #2

问题是,引用 cppreference

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

这很容易测试:如果你把

cin >> fact_num;

scanf("%d", &fact_num);

同样,但保留,您将在C++版本(或者更确切地说是IOStream版本)中获得与C one几乎相同的性能:cin >> num_of_inputscout

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

评论

0赞 chqrlie 12/7/2015
谢谢你的解释。然而引用 C++14 27.7.2.1.3: 类 basic_istream::哨兵 : 首先,如果 is.tie() 不是空指针,则函数调用 is.tie()->flush() 以将输出序列与任何关联的外部 C 流同步。但是,如果 is.tie() 的放置区域为空,则可以禁止此调用。此外,允许实现延迟对刷新的调用,直到调用 is.rdbuf()->underflow() 发生。如果在销毁哨兵对象之前没有发生此类调用,则可以完全消除对 flush 的调用。
0赞 chqrlie 12/7/2015
像往常一样,C++的事情比看起来要复杂得多。OP 的 C++ 库没有标准允许的那么高效。
0赞 Alex Lop. 12/7/2015
感谢您的 cppreference 链接。不过☺,我不喜欢打印屏幕中的“错误答案”
0赞 vitaut 12/7/2015
@AlexLop。哎呀,修复了“错误答案”问题=)。忘记更新另一个 cin(不过这不会影响时间)。
0赞 vitaut 12/7/2015
@chqrlie 是的,但即使在下溢情况下,与 stdio 解决方案相比,性能也可能更差。感谢您的标准参考。
44赞 Ilya Popov 12/7/2015 #3

当您同时使用两者时,使 s 更快的另一个技巧是调用iostreamcincout

cin.tie(nullptr);

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

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

在这种情况下,您需要确保在开始等待输入之前实际显示提示。有了上面的线,你就打破了这种束缚,变得独立。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型,甚至超越。使用,尤其是与手写解析一起使用,仍然可以提供更好的性能。scanfgetchargetchar_unlocked

我写了一篇文章,比较了几种在C++中输入数字的方法,对在线法官有用,但它只是俄语,对不起。但是,代码示例和最终表应该是可以理解的。

评论

1赞 chqrlie 12/7/2015
感谢您的解释和 +1 的解决方案,但您提出的替代方案在功能上不等同于 OPs 代码。两者都接受蚂蚁空格作为分隔符,同一行上可以有多个数字。std::readlinestd::stoicin >> x;scanf("%f", &x);