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

use*_*066 7 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;
}
Run Code Online (Sandbox Code Playgroud)

案例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;
}
Run Code Online (Sandbox Code Playgroud)

案例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;
  }
}
Run Code Online (Sandbox Code Playgroud)

案例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;
}
Run Code Online (Sandbox Code Playgroud)

案例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

Ren*_*ter 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 cpu上内部使用80位长的双精度数.当数字小于1.9E-4951时,我们得到一个浮点下溢,正如@James Kanze指出的那样.这只发生在案例2中.我不知道为什么这比标准化结果需要的时间更长,也许其他人可以解释.


Dav*_*men 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
Run Code Online (Sandbox Code Playgroud)

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

  • 首先,user334066没有使用Visual C++.前面已经说过编译器是gcc 4.6.其次,计算存储在double,而不是long double.无论编译器如何,这些计算都需要从/到IEEE 754双精度转换.该程序在大约708,500次迭代后仍将达到双重下溢. (2认同)