提问人:Alex Lop. 提问时间:12/7/2015 最后编辑:CommunityAlex Lop. 更新时间:12/7/2015 访问量:6411
C 和 C++ 中几乎相同的代码之间的执行时间差异很大 (x9)
Big difference (x9) in the execution time between almost identical code in C and C++
问:
我试图从 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.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 的解决方案
现在代码几乎相同,我按照此处的建议添加到 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 long
unsigned int
unsigned int
unsigned int
答:
这两个程序执行完全相同的操作。它们使用相同的精确算法,并且由于其低复杂性,它们的性能主要取决于输入和输出处理的效率。
无论哪种方式,用一侧和另一侧扫描输入似乎都不是很昂贵。事实上,在 C++ 中,它应该成本更低,因为转换类型在编译时是已知的,并且 C++ 编译器可以直接调用正确的解析器。这同样适用于输出。你甚至会写一个单独的调用,但 C 编译器已经足够好了,可以将其编译为对 的调用。scanf("%d", &fact_num);
cin >> fact_num;
printf("%s","\n");
putchar('\n');
因此,考虑到 I/O 和计算的复杂性,C++ 版本应该比 C 版本更快。
完全禁用缓冲会使 C 实现速度减慢到比 C++ 版本更慢的速度。AlexLop的另一项测试在最后一个之后产生了与C++版本相似的性能。它不像完全禁用缓冲那样慢,因为输出以小块的形式写入系统,而不是一次一个字节。stdout
fflush(stdout);
printf
这似乎指向您的 C++ 库中的特定行为:我怀疑您的系统实现了输出,并在请求输入时将输出刷新到 。一些 C 库也这样做,但通常仅在读取/写入终端时。www.spoj.com 站点完成的基准测试可能会将输入和输出重定向到文件或从文件重定向。cin
cout
cout
cin
AlexLop做了另一个测试:一次读取向量中的所有输入,然后计算和写入所有输出有助于理解为什么C++版本如此慢。它提高了 C 版本的性能,这证明了我的观点并消除了对 C++ 格式代码的怀疑。
高炉的另一项测试,将所有输出存储在一个中,并在最后的一次鼓风中冲洗,确实将C++性能提高到基本C版本。QED的。std::ostringstream
交错输入和输出似乎会导致 I/O 处理效率非常低下,从而破坏了流缓冲方案。性能降低 10 倍。
cin
cout
PS:您的算法不正确,因为在它变成之前会溢出并环绕。您可以通过使 或 如果这些类型之一大于 .也用作格式。你很幸运,www.spoj.com 的家伙在他们的基准上并不太严格。fact_num >= UINT_MAX / 5
fives *= 5
> fact_num
fives
unsigned long
unsigned long long
unsigned int
%u
scanf
编辑:正如 vitaux 后来解释的那样,这种行为确实是 C++ 标准规定的。 默认情况下绑定到。输入缓冲区需要重新填充的输入操作将导致刷新挂起的输出。在 OP 的实现中,似乎系统地冲洗,这有点矫枉过正且明显效率低下。cin
cout
cin
cout
cin
cout
伊利亚·波波夫(Ilya Popov)为此提供了一个简单的解决方案:除了以下方法外,还可以通过施放另一个魔法咒语来解开束缚:cin
cout
std::ios_base::sync_with_stdio(false);
cin.tie(nullptr);
另请注意,当使用 而不是 在 上生成行尾时,也会发生这种强制刷新。将输出行更改为更 C++ 惯用且看起来更无辜的行也会以同样的方式降低性能。std::endl
'\n'
cout
cout << num_of_trailing_zeros << endl;
评论
std::ostringstream
ostringstream
fact_num >= UINT_MAX / 5
vector
ostringstream
vector
ostringstream
sizeof(int) == sizeof(long long)
num_of_trailing_zeros += fact_num/fives;
fives
if (fives > UINT_MAX / 5) break;
问题是,引用 cppreference:
来自 std::cin 的任何输入、输出到 std::cerr 或程序终止都会强制调用 std::cout.flush()
这很容易测试:如果你把
cin >> fact_num;
跟
scanf("%d", &fact_num);
同样,但保留,您将在C++版本(或者更确切地说是IOStream版本)中获得与C one几乎相同的性能:cin >> num_of_inputs
cout
如果您保留但替换,也会发生同样的情况cin
cout << num_of_trailing_zeros << "\n";
跟
printf("%d", num_of_trailing_zeros);
printf("%s","\n");
一个简单的解决方案是解开束缚,正如伊利亚·波波夫(Ilya Popov)所提到的:cout
cin
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 的调用。
评论
is.tie
()->flush()
以将输出序列与任何关联的外部 C 流同步。但是,如果 is.tie()
的放置区域为空,则可以禁止此调用。此外,允许实现延迟对刷新的调用,直到调用 is.rdbuf()->underflow()
发生。如果在销毁哨兵对象之前没有发生此类调用,则可以完全消除对 flush 的调用。
当您同时使用两者时,使 s 更快的另一个技巧是调用iostream
cin
cout
cin.tie(nullptr);
默认情况下,当您从 中输入任何内容时,它会刷新 。如果执行交错输入和输出,可能会严重损害性能。这是为命令行界面使用完成的,您可以在其中显示一些提示,然后等待数据:cin
cout
std::string name;
cout << "Enter your name:";
cin >> name;
在这种情况下,您需要确保在开始等待输入之前实际显示提示。有了上面的线,你就打破了这种束缚,变得独立。cin
cout
从 C++ 11 开始,使用 iostreams 实现更好性能的另一种方法是与 一起使用,如下所示: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型,甚至超越。使用,尤其是与手写解析一起使用,仍然可以提供更好的性能。scanf
getchar
getchar_unlocked
我写了一篇文章,比较了几种在C++中输入数字的方法,对在线法官有用,但它只是俄语,对不起。但是,代码示例和最终表应该是可以理解的。
评论
std::readline
std::stoi
cin >> x;
scanf("%f", &x);
上一个:从终端发送一行C++输入
评论
std::ostringstream
std::cout
std::cout