在For循环中发生了什么GCC优化?

时间:2011-07-04 16:02:56

标签: c++ gcc

使用gcc 4.6和-O3,我使用简单时间命令

计时以下四个代码
#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循环中被硬编码。一般来说,对这个值进行硬编码会使事情比这里给出的情况慢。对于案例3尤其如此,如上所示几乎立即使用numIterations变量运行,表明James McNellis对整个循环进行优化是正确的。我不确定为什么将1e8硬编码到for循环中会阻止在案例3中删除循环或在其他情况下使事情变慢,但是,案例2的基本前提明显更慢更为真实。

区分装配输出给出了上面给出的情况

案例2和案例1:
movl $ 100000000,16(%esp)


movl $ 10000000,16(%esp)

案例2和案例4:
.long -652835029
.long 1072691150


.long -417264663
.long 1072693245

4 个答案:

答案 0 :(得分:8)

使用选项-S编译并查看生成的汇编程序输出(名为* .s的文件)​​。

编辑:

在程序3中,循环被删除,因为没有使用结果。

对于案例1,2和4,让我们做一些数学运算: 情况1的结果的基数10对数是1e7 * log10(0.999)= -4345(粗略地)。 对于情况2,我们得到1e8 * log10(0.999)= -43451。 对于情况4,它是1e8 * log10(0.9999)= -4343。 结果本身就是pow(10,对数)。

浮点单元在x86 / x64 cpus内部使用80位长的双精度数。 当数字小于1.9E-4951时,我们得到一个浮点下溢,正如@James Kanze指出的那样。这只发生在案例2中。 我不知道为什么这需要比标准化结果更长的时间,也许其他人可以解释。

答案 1 :(得分:8)

RenéRichter关于下溢的正确轨道。最小的正标准化数字约为2.2e-308。在f(n)= 0.999 ** n的情况下,在大约708,148次迭代之后达到该限制。其余的迭代都停留在非标准化的计算中。

这解释了为什么1亿次迭代所花费的时间略多于执行1000万次所需时间的10倍。前700,000是使用浮点硬件完成的。一旦你击中了非规范化数字,浮点硬件就会出现问题;乘法是用软件完成的。

请注意,如果重复计算正确计算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

在这里,您可以轻松地看到浮点硬件的速度比软件仿真快多少。

答案 2 :(得分:1)

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

以下是我进行测试时的时间结果:

  • 案例2:6.81 s
  • 案例3:0.00 s
  • 案例4:0.20 s

我看了所有三个测试的集合。

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

main:
        xorl    %eax, %eax
        ret

案例2和案例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,所以当然它应该花费更少的时间。

答案 3 :(得分:1)

对于它的价值,我已经对所有四个版本进行了一些测试 结果如下:

  • 首先,我的速度与你的速度相同。或多或少:我有一台较慢的机器,我看到测试1,3和4之间存在明显的差异。但是它们仍然比测试2快两个数量级。

  • 测试3是迄今为止最快的:查看生成的代码显示g ++确实删除了循环,因为结果未被使用。

  • 测试1的速度比测试4快十分之多。关于人们的期望,或多或少。

  • 测试1,3和4 之间生成代码的唯一区别是常量。循环中没有任何区别。

所以差异不是编译器优化的问题。我可以 只推测它与下溢处理有某种关系。 (但 那么,我预计循环1也会放缓。)