首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >GCC的优化在这个循环中发生了什么?

GCC的优化在这个循环中发生了什么?
EN

Stack Overflow用户
提问于 2011-07-04 16:02:56
回答 4查看 608关注 0票数 7

使用gcc 4.6和-O3,我使用简单的time命令对以下四个代码进行计时

代码语言:javascript
复制
#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秒内运行

代码语言:javascript
复制
#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秒

代码语言:javascript
复制
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秒

代码语言:javascript
复制
#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

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 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亿次迭代。利用这点,

代码语言:javascript
复制
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

在这里,您可以很容易地看到浮点硬件比软件仿真快得多。

票数 8
EN

Stack Overflow用户

发布于 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所指出的。这只会发生在第二种情况下。我不知道为什么这比一个规范化的结果要花更长的时间,也许其他人可以解释。

票数 8
EN

Stack Overflow用户

发布于 2011-07-04 17:09:41

我在64位Linux上运行g++ (Ubuntu 4.4.3-4ubuntu5) 4.4.3。我和你一样使用-O3。

以下是我在做测试时的计时结果:

  • Case 2: 6.81 s
  • Case 3: 0.00 s
  • Case 4: 0.20 s

我看了三个测试的集合。

案例3是快速的,因为GCC确实优化了整个for循环。主要功能很简单(删除标签和.cfi*语句之后):

代码语言:javascript
复制
main:
        xorl    %eax, %eax
        ret

对于Case 2和Case 3,程序集中唯一的不同之处可能是表示0.999或0.999999的常量:

代码语言:javascript
复制
$ 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,因此,当然应该少花一些时间。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/6573746

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档