为什么仅 -fno-signed-0 就可以实现优化,而似乎也需要 -ffinite-math-only (gcc)

Why does -fno-signed-zeros alone enable optimization, for which seemingly also -ffinite-math-only is needed (gcc)

本文关键字:-ffinite-math-only gcc 就可以 -fno-signed-0 实现 优化 为什么      更新时间:2023-10-16

手册页中没有任何内容表明-fno-signed-zeros暗示-ffinite-math-only

-FNO 符号零

允许对忽略零符号的浮点算术进行优化。IEEE 算术指定了不同行为+0.0-0.0值,然后禁止简化表达式,例如x+0.00.0*x(即使使用-ffinite-math-only)。 此选项意味着零结果的符号不显著。

默认值为 -fsigned-zero。

但是,如果情况确实如此,可以解释一些意见。我的代码中的问题归结为以下有点愚蠢的示例:

#include <complex>
std::complex<double> mult(std::complex<double> c, double im){
std::complex<double> jomega(0.0, im);
return c*jomega;
}

编译器会倾向于将乘法c*=jomega优化为类似于c={-omega*c.imag(), omega*c.real()}但是,IEEE 754 合规性和至少以下极端情况会阻止它:

A) 有符号零,例如omega=-0.0c={0.0, -0.0}

(c*jomega).real() = 0.0*0.0-(-0.0)*(-0.0) =  0.0
-c.imag()*omega   = -(-0.0)*(-0.0)        = -0.0  //different!

B) 无穷大,例如omega=0.0c={inf, 0.0}

(c*jomega).real() = inf*0.0-0.0*0.0 =  nan
-c.imag()*omega   = -(0.0)*(0.0)    = -0.0     //different!

C)nans,例如omega=0.0c={inf, 0.0}

(c*jomega).real() = nan*0.0-0.0*0.0 =  nan
-c.imag()*omega   = -(0.0)*(0.0)    = -0.0    //different!

这意味着,我们必须同时使用-ffinite-math-only(对于 B 和 C)和-fno-signed-zeros(对于 A),以便进行上述优化。

但是,即使只打开了-fno-signed-zeros,如果我正确理解生成的汇编程序,gcc 也会执行上述优化(或查看下面的列表以查看效果):

mult(std::complex<double>, double):
mulsd   %xmm2, %xmm1
movapd  %xmm0, %xmm3
mulsd   %xmm2, %xmm3
movapd  %xmm1, %xmm0
movapd  %xmm3, %xmm1
xorpd   .LC0(%rip), %xmm0
ret
.LC0:
.long   0
.long   -2147483648
.long   0
.long   0

我的第一个问题是,这可能是一个错误 - 但我手头的所有最近的 gcc 版本都会产生相同的结果,所以我可能错过了一些东西。

因此,我的问题是,为什么 gcc 仅在打开-fno-signed-zeros和没有-ffinite-math-only的情况下执行上述优化?


清单:

单独的mult.cpp以避免在编译过程中进行时髦的预计算

#include <complex>
std::complex<double> mult(std::complex<double> c, double im){
std::complex<double> jomega(0.0, im);
return c*jomega;
}

主.cpp:

#include <complex>
#include <iostream>
#include <cmath>
std::complex<double> mult(std::complex<double> c, double im);

int main(){
//(-nan,-nan) expected:
std::cout<<"case INF: "<<mult(std::complex<double>(INFINITY,0.0),
0.0)<<"n";
//(nan,nan) expected:
std::cout<<"case NAN: "<<mult(std::complex<double>(NAN,0.0),  0.0)<<"n"; 
}

编译并运行:

>>> g++ main.cpp mult.cpp -O2 -fno-signed-zeros -o mult_test
>>> ./mult_test
case INF: (-0,-nan)   //unexpected!
case NAN: (-0,nan)    //unexpected!

我这边是一个误解,认为复数乘法的定义与在学校学习的方式相同。

基本上,C++标准与复杂的乘法无关,因此可能必须参考C标准。仅从C99开始,复数是标准(附录G)的一部分,该标准尚未唯一定义复数乘法的所有结果。

最重要的定义是:

  1. 当两个部分都为零时,复数为零(0.0-0.0)。
  2. 当两个部分都是有限的并且不nans时,复数是有限的。
  3. 当实数或虚部(或两者兼而有之)inf-inf时,复数是无限的(即使另一个是nan)。

它没有定义什么是复数nan,所以如果一个部分是nan的,我们可以认为复数被nan(只要没有无限部分)。

该标准继续说,学校乘法在大多数情况下应该成立,但也

如果一个操作数是无穷大,

另一个操作数是非零有限数或无穷大,则算子的结果是无穷大;

这意味着,例如,(1.0+0j)*(inf+inf*j)应该是无限的(inf+inf*j可能是最有意义的),但不是nan+nan*j,因为通常的公式就是这种情况。

在我下面的SO问题中有更多关于这个主题的内容。

鉴于编译器具有一定的自由度产生结果,我们可以看到,通过__multdc3使用的实现与简化的学校公式之间的区别仅在考虑有符号零的情况下,即(-0,-0)vs.(0,-0)等等(请参阅下面进一步测试的程序列表或在此处实时查看)。

这意味着,gcc 的行为是可以的,因为它使用了标准的未定义行为。有人可能会争辩说,这是错过了对叮当声的优化。

注意:还有一个"错误报告":https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84891


#include <complex>
#include <iostream>
#include <cmath>
#include <cfloat>
#include <vector>

int get_type(std::complex<double> c){
if(std::isinf(c.real()) || std::isinf(c.imag()))
return 2;
if(std::isnan(c.real()) || std::isnan(c.imag()))
return 1;
return 0;
}
void do_mult(double b, double c, double d){
std::complex<double> school(-b*d, b*c);
std::complex<double> f(0.0,b);
std::complex<double> s(c,d);
auto cstd=f*s;
int type1=get_type(school);
int type2=get_type(cstd);
#ifdef INFINITE_MATH
//not special,    usual            
if(type1!=type2 || (type1==0 &&  (cstd!=school))){
std::cout<<"(0.0,"<<b<<")*("<<c<<","<<d<<")="<<school<<"vs."<<cstd<<"n";
}
#endif
#ifdef SIGNED_ZERO_MATH
//       signed zero
if(type1!=type2 || (type1==0 &&  (1.0/cstd.real()!=1.0/school.real() || 1.0/cstd.imag()!=1.0/school.imag() ))){
std::cout<<"(0.0,"<<b<<")*("<<c<<","<<d<<")="<<school<<"vs."<<cstd<<"n";
}
#endif
}
int main(){
std::vector<double> numbers{0.0, -0.0, 1.0, INFINITY, -INFINITY, NAN, DBL_MAX, -DBL_MAX};
for(double b: numbers)
for(double c: numbers)
for(double d: numbers)
do_mult(b,c,d);
}

要构建/运行使用:

g++ main.cpp -o main -std=c++11 -DINFINITE_MATH -DSIGNED_ZERO_MATH && ./main