【问题标题】:Storing a variable changes result of trivial operation with -O1使用 -O1 存储变量更改简单操作的结果
【发布时间】:2017-02-17 21:51:21
【问题描述】:

我有一个 Fortran 程序,它在 32 位系统中使用 -O0-O1 给出不同的结果。追踪差异,我想出了以下测试用例(test.f90):

program test
implicit none
character foo
real*8 :: Fact,Final,Zeta,rKappa,Rnxyz,Zeta2

read(5,*) rKappa
read(5,*) Zeta
backspace(5)
read(5,*) Zeta2
read(5,*) Rnxyz

Fact=rKappa/Sqrt(Zeta**3)

write(6,'(ES50.40)') Fact*Rnxyz

Fact=rKappa/Sqrt(Zeta2**3)
Final = Fact*Rnxyz
write(6,'(ES50.40)') Final

end program test

使用这个data 文件:

4.1838698196228139E-013
20.148674000000000     
-0.15444754236171612

程序应该写出完全相同的数字。请注意Zeta2Zeta 相同,因为再次读取相同的数字(这是为了防止编译器意识到它们是相同的数字并隐藏问题)。唯一的区别是在写入时首先“即时”完成一个操作,然后将结果保存在一个变量中并打印该变量。

现在我用 gfortran 4.8.4(Ubuntu 14.04 版本)编译并运行它:

$ gfortran -O0 -m32 test.f90 && ./a.out < data
   -7.1447898573566615177997578153994664188136E-16
   -7.1447898573566615177997578153994664188136E-16

$ gfortran -O1 -m32 test.f90 && ./a.out < data
   -7.1447898573566615177997578153994664188136E-16
   -7.1447898573566605317236262891347096541529E-16

因此,-O0 的数字是相同的,-O1 则不同。

我尝试使用-fdump-tree-optimized检查优化代码:

  final.10_53 = fact_44 * rnxyz.9_52;
  D.1835 = final.10_53;
  _gfortran_transfer_real_write (&dt_parm.5, &D.1835, 8);
  [...]
  final.10_63 = rnxyz.9_52 * fact_62;
  final = final.10_63;
  [...]
  _gfortran_transfer_real_write (&dt_parm.6, &final, 8);

我看到的唯一区别是,在一种情况下打印的数字是fact*rnxyz,在另一种情况下是rnxyz*fact。这能改变结果吗?根据高性能标记的回答,我想这可能与哪个变量何时进入哪个寄存器有关。我还尝试查看使用-S 生成的程序集输出,但我不能说我理解它。

然后,没有-m32 标志(在 64 位机器上),数字也是相同的......

编辑:如果我添加-ffloat-store-mfpmath=sse -sse2,数字是相同的(请参阅最后的here)。我想,当我在 i686 机器上编译时,这是有道理的,因为编译器默认使用 387 数学。但是当我在 x86-64 机器上编译时,使用-m32,根据文档,它不应该是必需的:

-mfpmath=sse [...]

对于 i386 编译器,您必须使用 -march=cpu-type-msse-msse2 开关来启用 SSE 扩展并使此选项生效。 对于 x86-64 编译器,默认启用这些扩展。

[...]

这是 x86-64 编译器的默认选择。

也许-m32 使这些“默认值”无效?但是,运行 gfortran -Q --help=target 表示 mfpmath 为 387 并且 msse2 已禁用...

【问题讨论】:

  • 检查汇编生成的代码。 O1 可能会对您发布的代码进行一些优化。您可能对stackoverflow.com/questions/19618679/… 感兴趣。
  • @Harald 我用-S 检查了输出,但这开始太模糊了......无论如何,我也看不出有任何区别。我正在用(AFAICT)相关部分更新问题。
  • @Harald 我在测试中犯了一个错误,所以诊断结果有点不同。似乎因素的顺序发生了变化。

标签: gcc optimization floating-point fortran gfortran


【解决方案1】:

评论太长,但更多的是怀疑而不是答案。 OP 写

唯一的区别是首先操作是“即时”完成的 写入时,然后将结果保存在变量中,然后 变量被打印出来。

这让我想到了 x86_64 架构的内部 80 位 f-p 算法。当中间值从 80 位修剪到 64 位时,会影响 f-p 算术运算序列的精确结果。这就是不同编译器优化级别可能不同的东西。

另请注意,O1 版本代码打印的两个数字之间的差异出现在第 15 位十进制数字处,这与 64 位 f-p 算术中可用的精度限制有关。

更多的摆弄给予

1 01111001100 1001101111011110011111001110101101101100011000001110

作为

的 IEEE-754 表示
-7.1447898573566615177997578153994664188136E-16

1 01111001100 1001101111011110011111001110101101101100011000001101

作为

的 IEEE-754 表示
-7.1447898573566605317236262891347096541529E-16

这两个数字的有效数字相差1。在O0,您的编译器可能遵守 IEEE-754 的 fp 算术规则(这些规则对诸如在低位四舍五入之类的问题是严格的)但在O1 仅遵守 Fortran 更宽松的算术视图. (Fortran 标准不需要使用 IEEE-754 算法。)

您可能会发现一个编译器选项可以在更高级别的优化中强制遵守 IEEE-754 规则。您可能还会发现,这种坚持会花费您大量的运行时间。

【讨论】:

  • 不幸的是,在实际应用中,这种不匹配增加到超过 1e-08。可能算法和测试用例过于敏感,也可能除了这个之外还有其他原因导致最终的差异。 (注意我已经修改了问题中的结论,实际上因素的顺序有所不同。)
  • @Jellby 这种差异有什么实际意义吗,还是你只是在预测下一个圣诞节的天气并得到两个不同的答案?
  • @VladimirF 这很重要,因为它使测试套件在带有-O0 的 32 位机器上失败,因为它接受高达 1e-08 的差异。但鉴于我使用上述示例得到的不同结果,我预计测试会以-O1 失败,所以问题可能有所不同。
  • 当然,但是测试服中的限制是否相关?
  • 如前面的 cmets 所示,您似乎选择了 387 代码生成。在 O1 中,一些数据缩小舍入步骤被跳过。如果您想始终避免额外的精度,您可以设置例如-march=native。如果您希望根据您的 387 精度模式设置保持一致的精度,您可以声明 real (10) 或 real*10。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-07
  • 2011-02-16
  • 2011-12-13
  • 2013-08-06
  • 1970-01-01
  • 1970-01-01
  • 2017-03-22
相关资源
最近更新 更多