使用gcc 4.6和-O3,我使用简单的time命令对以下四个代码进行计时
#include <iostream>
int main(int argc, char* argv[])
{
double val = 1.0;
unsigned int numIterations = 1e7;
for(unsigned int ii = 0;ii < numIterations;++ii) {
val *= 0.999;
}
std::cout<<val<<std::endl;
}案例1在0.09秒内运行
#include <iostream>
int main(int argc, char* argv[])
{
double val = 1.0;
unsigned int numIterations = 1e8;
for(unsigned int ii = 0;ii < numIterations;++ii) {
val *= 0.999;
}
std::cout<<val<<std::endl;
}案例2运行时间为17.6秒
int main(int argc, char* argv[])
{
double val = 1.0;
unsigned int numIterations = 1e8;
for(unsigned int ii = 0;ii < numIterations;++ii) {
val *= 0.999;
}
}案例3运行0.8秒
#include <iostream>
int main(int argc, char* argv[])
{
double val = 1.0;
unsigned int numIterations = 1e8;
for(unsigned int ii = 0;ii < numIterations;++ii) {
val *= 0.999999;
}
std::cout<<val<<std::endl;
}案例4运行0.8秒
我的问题是,为什么第二个案件比所有其他案件都慢得多?案例3显示,删除cout将使运行时恢复到预期的水平。案例4表明,改变乘法器也大大减少了运行时。在第2种情况下没有发生什么优化或优化,为什么?
更新:
当我最初运行这些测试时,没有单独的变量numIterations,该值在for循环中被硬编码。一般来说,硬编码这个值会使事情比这里给出的情况运行得慢。对于使用上面所示的numIterations变量几乎立即运行的案例3,这尤其如此,这表明James McNellis关于整个循环被优化的说法是正确的。我不知道为什么硬编码到for循环中的1e8会阻止在第3种情况下删除循环,或者在其他情况下会使事情变得更慢,然而,情况2明显慢的基本前提甚至更加正确。
为上述情况提供的装配输出给出
案例2和案例1:
100000000美元,16%(%)
10000000美元,16%(%)
案例2和案例4:
.long -652835029
.long 1072691150
.long -417264663
.long 1072693245
发布于 2011-07-04 18:56:00
勒内·里希特在地下水流问题上走在正确的轨道上。最小正归一化数约为2.2e-308。当f(n)=0.999**n时,经过大约708,148次迭代,达到这个极限。其余的迭代都被未规范化的计算所困扰。
这就解释了为什么1亿次迭代所花费的时间略多于执行1000万次所需时间的10倍。前70万是使用浮点硬件完成的。一旦你击中非正态化的数字,浮点硬件就会打牌;乘法是在软件中完成的。
请注意,如果重复计算正确地计算0.999**N,则情况不是这样。最后乘积将达到零,然后再一次用浮点硬件进行乘法。但情况并非如此,因为0.999 *(最小的非正态数)是最小的非正态数。继续生产的产品最终会触底。
我们能做的就是改变指数。0.999999的指数将使连续乘积保持在归一化数的范围内,进行7.08亿次迭代。利用这点,
Case A : #iterations = 1e7, exponent=0.999, time=0.573692 sec
Case B : #iterations = 1e8, exponent=0.999, time=6.10548 sec
Case C : #iterations = 1e7, exponent=0.999999, time=0.018867 sec
Case D : #iterations = 1e8, exponent=0.999999, time=0.189375 sec在这里,您可以很容易地看到浮点硬件比软件仿真快得多。
发布于 2011-07-04 16:09:26
使用选项-S编译并查看生成的汇编程序输出(文件名为*.s)。
编辑:
在程序3中,循环被删除,因为结果不被使用。
对于案例1、2和4,让我们做一些计算:案例1的结果的基数10对数是1e7 * log10(0.999) = -4345 (大致)。对于第2种情况,我们得到1e8*log10 10(0.999)= -43451。对于案例4,是1e8*log10 10(0.9999)= -4343。结果本身是pow(10,对数)。
浮点单元在x86/x64 cpus上内部使用80位长的双倍。当这个数字小于1.9E-4951时,我们得到一个浮点下流动,正如James所指出的。这只会发生在第二种情况下。我不知道为什么这比一个规范化的结果要花更长的时间,也许其他人可以解释。
发布于 2011-07-04 17:09:41
我在64位Linux上运行g++ (Ubuntu 4.4.3-4ubuntu5) 4.4.3。我和你一样使用-O3。
以下是我在做测试时的计时结果:
我看了三个测试的集合。
案例3是快速的,因为GCC确实优化了整个for循环。主要功能很简单(删除标签和.cfi*语句之后):
main:
xorl %eax, %eax
ret对于Case 2和Case 3,程序集中唯一的不同之处可能是表示0.999或0.999999的常量:
$ diff case2.s case4.s
121,122c121,122
< .long 3642132267
< .long 1072691150
---
> .long 3877702633
> .long 1072693245这是程序集中唯一的区别。GCC对案例2和案例4进行了同样的优化,但案例2所用的时间是案例4的30倍。我的结论是,x64体系结构上的浮点乘法必须花费可变的时间,这取决于你所乘的值。不应该是一个非常令人惊讶的陈述,但如果一个更了解x64的人能向我们解释为什么是这样的话,那就太好了。
我没有仔细研究案例1,因为您只执行1e7迭代,而不是1e8,因此,当然应该少花一些时间。
https://stackoverflow.com/questions/6573746
复制相似问题