我有一个场景,在这个场景中,我必须从用户那里获取n个输入并将其存储在一个数组中,我还需要找到数组中所有元素的总和。因为它是在codeforce中,我想知道做它的最优方法。
通常,I/O操作被认为是臭名昭著的使用大量的CPU时间。如果我经常从I/O和处理之间切换,而不是先接受普通输入,然后再处理总和,那么在大规模输入(即1000个输入)中是否会有性能影响?
我正在使用c++语言和在线裁判(Codeforces)。
第一种方法:循环n次,从输入流中读取n个输入,并将它们一个接一个地放入数组中。然后把它们都加起来。
int n, sum=0;
cin >> n;
int *myarray = new int[n];
for(int i=0; i<n; i++)
{
cin >> myarray[i];
}
for(int i=0; i<n; i++)
{
sum += myarray[i];
}
第二种方法:循环n次,读取n个输入。每次,我都将它放入一个数组中,并将刚刚作为输入的数字相加。
int n, sum=0;
cin >> n;
int *myarray = new int[n];
for(int i=0; i<n; i++)
{
cin >> myarray[i];
sum += myarray[i];
}
在这样的程序中,只有一个线程接受用户输入,将其存储在数组中并求和,这不会有(明显的)区别-无论如何都会发生相同的操作。
更明显的优化将是丢失数组。如果所需的输出只是求和,则可以直接对输入求和而不存储它们:
int sum = 0;
for(int i=0; i<n; i++)
{
int temp;
cin >> temp;
sum += temp;
}
我怀疑在实践中,尽管与函数中的其他操作相比,cin是如此缓慢,任何差异都不太可能是可测量的。
也就是说,如果我们忽略cin的缓慢性并关注函数生成的代码的质量,我看到了单循环方法的几个优点。
- 第一个例子有两次循环控制指令,这些指令很便宜,但不是免费的。
- 最近写入的内存位置几乎肯定仍然在L1缓存中,如果迭代次数很大,在前一个循环中写入的内存位置可能不在L1缓存中。
在一个循环中做太多不同的事情的一个可能的缺点是你可能会用完"callee-save"寄存器,迫使代码使用堆栈的变量,否则可能存在于寄存器,但我不认为这将是一个问题,在大多数体系结构。
在查看godbolt上编译的代码时,我注意到的另一件事是,因为cin接受引用而不是返回值,所以"n"不能跨函数调用驻留在寄存器中。因此,调用cin的循环必须在每次迭代时重新加载它。
严格地说,在一个循环中完成它们会更快,但这不是您应该担心的差异。
对于Codeforces,您可能想要更快的io,所以我建议您使用scanf
和printf
而不是cin
和cout
,它们已知更快。
如果你想使用cin
和cout
,你也可以添加行,这使他们更快,你可以检查为什么在这里
ios_base::sync_with_stdio(false);
cin.tie(NULL);
为什么?
当这段代码转换为汇编程序时,循环只是一个条件跳转语句,根据条件的不同,跳转可能被执行或不执行,只有当条件被求值时我们才能知道。
我们肯定不想等待条件求值,所以我们有分支预测(它可以通过多种方式完成),它会进行一些计算来估计是否采取了分支。
在条件被求值之前,我们遵循我们的假设,并执行指令,而不将结果提交到寄存器或内存中。
如果我们的估计是正确的,这些结果被提交(我们自然地继续执行),如果不是,我们只是刷新管道(扔掉那些指令和它们的结果)。
我们的性能仍然会因为预测失误而降低(我们不需要计算的结果),这就是为什么减少流指令的控制有助于性能。
对于你的例子,将循环分成两个,将产生两个条件跳转指令,而不是一个,这会增加错过的惩罚。
有些编译器会执行循环展开来解决与类似的问题。
只要你不使用HPC(高性能计算)应用程序,或者真的需要不惜一切代价进行更好的优化,你就不需要担心这个。
不如专注于改进算法本身,就像@Mureinik说的,如果你的问题只需要求和,而不需要进一步使用数字本身,你可以直接计算求和。