【问题标题】:Big difference (x9) in the execution time between almost identical code in C and C++C 和 C++ 中几乎相同的代码在执行时间上的巨大差异 (x9)
【发布时间】:2016-03-11 09:13:12
【问题描述】:

我试图从 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 内存 3.3M

但后来我看到一些 cmets 声称他们的时间执行小于 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 内存 2.1M

现在代码几乎相同,我按照here 的建议将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");,以补偿cout &lt;&lt; num_of_trailing_zeros &lt;&lt; "\n";operator&lt;&lt; 的双重调用。

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

这是为什么呢?

编辑

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

【问题讨论】:

  • C++ streams are extremely slow by design. 因为缓慢而稳定地赢得比赛。 :P(在我被激怒之前运行
  • 缓慢并非来自安全性或适应性。它的所有流标志都被过度设计了。
  • @AlexLop。使用std::ostringstream 来累积输出并将其发送到std::cout 最后一次性 将时间降低到0.02。在循环中使用std::cout 在他们的环境中只是速度较慢,我认为没有简单的方法可以改进它。
  • 没有其他人担心这些时间是使用 ideone 获得的吗?
  • @Olaf:恐怕我不同意,这种问题对于所有选择的标签来说都是非常重要的。 C 和 C++ 通常足够接近,以至于性能上的这种差异需要一个解释。我很高兴我们找到了。或许 GNU libc++ 应该因此得到改进。

标签: c++ c performance gcc iostream


【解决方案1】:

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

一边用scanf("%d", &amp;fact_num); 扫描输入,另一边用cin &gt;&gt; fact_num; 扫描输入,这两种方法的成本似乎都不是很高。事实上,它在 C++ 中的成本应该更低,因为转换的类型在编译时是已知的,并且正确的解析器可以由 C++ 编译器直接调用。输出也是如此。您甚至还特意为printf("%s","\n"); 编写了一个单独的调用,但C 编译器足以将其编译为对putchar('\n'); 的调用。

所以从 I/O 和计算的复杂性来看,C++ 版本应该比 C 版本快。

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

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

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

Blastfurnace 的另一项测试,将所有输出存储在 std::ostringstream 中并在最后一次爆炸中刷新,确实将 C++ 性能提高到基本 C 版本的性能。 QED。

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

PS:你的算法对于fact_num &gt;= UINT_MAX / 5 是不正确的,因为fives *= 5 在它变成&gt; fact_num 之前会溢出并环绕。如果这些类型之一大于unsigned int,您可以通过将fives 设置为unsigned longunsigned long long 来纠正此问题。也使用%u 作为scanf 格式。你很幸运 www.spoj.com 上的人在他们的基准测试中并不太严格。

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

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

cin.tie(nullptr);

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

【讨论】:

  • 关于流冲洗你可能是对的。在std::ostringstream 中收集输出并在最后全部输出一次,可以将时间缩短到与 C 版本相同的水平。
  • @DavidC.Rankin:我冒险了一个猜想(cout 一读到 cin 就脸红了),设计了一种方法来证明它,AlexLop 实现了它,它确实提供了令人信服的证据,但 Blastfurnace 想出了一个不同的证明我观点的方法和他的测试提供了同样令人信服的证据。我把它当做证明,不过当然不是完全形式化的证明,看C++源码就可以了。
  • 我尝试使用ostringstream 作为输出,它给出了 Time 0.02 QED :)。关于fact_num &gt;= UINT_MAX / 5,好点!
  • 将所有输入收集到vector,然后处理计算(没有ostringstream)给出相同的结果! 时间 0.02。结合vectorostringstream 并没有进一步改善它。相同时间 0.02
  • 一个更简单的修复,即使sizeof(int) == sizeof(long long) 是这样的:在num_of_trailing_zeros += fact_num/fives; 之后的循环体中添加一个测试,以检查fives 是否已达到最大值:if (fives &gt; UINT_MAX / 5) break;跨度>
【解决方案2】:

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

cin.tie(nullptr);

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

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风格,甚至超越scanf。将getchar 尤其是getchar_unlocked 与手写解析一起使用仍然可以提供更好的性能。

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

【讨论】:

  • 感谢您的解释和解决方案的 +1,但您提出的 std::readlinestd::stoi 替代方案在功能上不等同于 OPs 代码。 cin &gt;&gt; x;scanf("%f", &amp;x); 都接受蚂蚁空格作为分隔符,同一行可以有多个数字。
【解决方案3】:

问题在于,引用cppreference

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

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

cin >> fact_num;

scanf("%d", &fact_num);

cin &gt;&gt; num_of_inputs 相同,但保留cout,您将在 C++ 版本(或者,更确切地说是 IOStream 版本)中获得与 C 版本几乎相同的性能:

如果你保留cin 但替换

cout << num_of_trailing_zeros << "\n";

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

如 Ilya Popov 所述,一个简单的解决方案是解开 coutcin

cin.tie(nullptr);

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

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

【讨论】:

  • 感谢您的解释。然而引用 C++14 27.7.2.1.3: Class basic_istream::sentry : 首先,如果is.tie() 不是空指针,函数调用is.tie()-&gt;flush() 来同步输出与任何关联的外部 C 流的序列。除非is.tie() 的 put 区域为空,则可以抑制此调用。此外,允许实现将调用延迟到刷新,直到发生is.rdbuf()-&gt;underflow() 的调用。如果在哨兵对象被销毁之前没有发生这样的调用,则可以完全消除对刷新的调用。
  • 与 C++ 一样,事情比看起来更复杂。 OP 的 C++ 库没有标准允许的那么高效。
  • 感谢 cppreference 链接。我不喜欢打印屏幕上的“错误答案”☺
  • @AlexLop。糟糕,修复了“错误答案”问题 =)。忘记更新其他 cin(但这不会影响时间)。
  • @chqrlie 是的,但即使在下溢情况下,与 stdio 解决方案相比,性能也可能更差。感谢您的标准参考。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
  • 2019-12-28
  • 2020-04-20
  • 2021-03-07
  • 1970-01-01
  • 2017-05-08
  • 2018-04-18
相关资源
最近更新 更多