C++ 标准::晶圆厂(a*b)和标准::晶圆厂(a)*标准::晶圆厂(b)之间的差异

C++ 标准::晶圆厂(a*b)和标准::晶圆厂(a)*标准::晶圆厂(b)之间的差异,c++,x86,g++,compiler-optimization,clang++,C++,X86,G++,Compiler Optimization,Clang++,我正在编写一些数字代码,我正在查看编译器的输出。有一个特别的案例让我感到奇怪: 在实数中,它认为abs(a)*abs(b)=abs(a*b)。我希望在浮点数中也会出现同样的情况。然而,优化既不是由clang执行的,也不是由g++执行的,我想知道我是否遗漏了一些细微的差别。然而,两个编译器都意识到abs(abs(a)*abs(b))=abs(a)*abs(b) 以下是相关代码: #include<cmath> double fabsprod1(double a, double b)

我正在编写一些数字代码,我正在查看编译器的输出。有一个特别的案例让我感到奇怪:

在实数中,它认为
abs(a)*abs(b)=abs(a*b)
。我希望在浮点数中也会出现同样的情况。然而,优化既不是由clang执行的,也不是由g++执行的,我想知道我是否遗漏了一些细微的差别。然而,两个编译器都意识到
abs(abs(a)*abs(b))=abs(a)*abs(b)

以下是相关代码:

#include<cmath>

double fabsprod1(double a, double b) {
    return std::fabs(a*b);
}
double fabsprod2(double a, double b) {
    return std::fabs(a) * std::fabs(b);
}
double fabsprod3(double a, double b) {
    return std::fabs(std::fabs(a) * std::fabs(b));
}
#包括
双fabsprod1(双a,双b){
返回标准::晶圆厂(a*b);
}
双fabsprod2(双a,双b){
返回标准::晶圆厂(a)*标准::晶圆厂(b);
}
双fabsprod3(双a、双b){
返回标准::晶圆厂(标准::晶圆厂(a)*标准::晶圆厂(b));
}
下面是godbolt中令人困惑的编译器输出与gcc-10.1(撰写本文时的当前稳定版本)和-O3:

值得注意的是,即使使用-Ofast(据我所知,它对允许的转换更为宽松),也不会执行此优化


正如@Scheff在评论中指出的,double和float不是实数。但我也看不到浮点类型的角点情况,例如获取无穷大或NaN作为参数,可能会产生不同的输出。

整数(
int
)的类似问题:

这与您关于“实数/浮点数”的说法相矛盾


总的来说,您不应该期望编译器为您简化数学问题。也就是说,一些优化是可能的。请提供您看到优化的文档或类似示例。

我相信我找到了一个反例。我将此作为一个单独的答案发布,因为我不认为这与整数的情况类似

在我考虑的案例中,我忽略了改变浮点运算的舍入模式的可能性。问题是,当GCC(我猜)在编译时优化“已知”数量时,他似乎忽略了这一点。考虑下面的代码:

#include <iostream>
#include <cmath>
#include <cfenv>

double fabsprod1(double a, double b) {
    return std::fabs(a*b);
}
double fabsprod2(double a, double b) {
    return std::fabs(a) * std::fabs(b);
}

int main() {
        std::fesetround(FE_DOWNWARD);
        double a  = 0.1;
        double b = -3;
        std::cout << std::hexfloat;
        std::cout << "fabsprod1(" << a << "," << b << "): " << fabsprod1(a,b) << "\n";
        std::cout << "fabsprod2(" << a << "," << b << "): " << fabsprod2(a,b) << "\n";
#ifdef CIN
        std::cin >> b;
#endif
}

值得注意的是,只需要用O1(我认为完全可靠的)来改变输出,这种方式对我来说似乎并不合理。 输出为-DCIN时

fabsprod1(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
fabsprod2(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333333p-2
fabsprod1(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
fabsprod2(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
如果输出中没有-dcs,则为

fabsprod1(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
fabsprod2(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333333p-2
fabsprod1(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
fabsprod2(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
编辑:Peter Cordes(感谢您的评论)指出,这个令人惊讶的结果是因为我没有告诉GCC尊重舍入模式的改变。通过使用以下命令进行构建,可以实现预期的结果:

g++ -O1 -frounding-math -march=native main2.cpp && ./a.out

(在我的机器上也适用于O2和O3)。

从数学意义上讲,
double
float
都不是实数。因此,可能是
abs(a)*abs(b)=abs(a*b)
在所有(边缘)情况下都无效,因此不适用于优化。@Scheff感谢您的评论,我怀疑有一些特殊情况,因此我问这些类型是否存在细微差异。我曾经想过,如果一个人把Inf、-Inf或NaN作为一个参数,会发生什么。不过,到目前为止,我还看不到。值得注意的是,即使使用-Ofast(我认为它允许某些转换在某些情况下无效),我也看不到第二个vandpd得到优化。我会相应地修改这个问题,我只是试着把问题转移到积分上,这是很明显的。(例如,假设2-补码,任何有符号整数类型的最小值都没有正值。)然后,我记得浮点有一个单独的符号位。如果有原因的话,这可能是一个非常复杂的原因…;-)@是的,有一个单独的符号位。在链接的组件中可以看到,ABS操作是通过使用预定义掩码使用AND(
vandpd
)指令简单地重置该位来实现的。此外,没有任何角落的情况下检查。两个版本之间的区别在于符号位是在乘法操作数(2x和)上重置还是在乘法结果(1x和)上重置。与整数(int)相比的问题实际上是,没有-DCIN,编译器通过简单引用编译时计算的值来优化这两个乘积的计算。该值存储在.LC1。使用-DCIN,产品在编译时计算。这当然是一个优化失误,但对此没有要求。同样有趣的是,该标准不要求浮点的编译时计算具有与执行时计算相同的结果:IIRC,
-fno舍入数学
已经是GCC的默认值。(假定为默认舍入模式)。即使启用了该功能,或者甚至
-ffast math
,我们仍然没有得到这种优化。但是,确实存在一些不安全的情况,使用一些编译器选项(以及上的pragma STDC FENV_访问),所以,也许这就是为什么编译器选择不去寻找它。没有正确地告诉GCC的优化器尊重编译时常量传播的fesetround,这就是为什么您的优化结果令人惊讶的原因。@PeterCordes感谢您向我指出这个选项,我没有意识到这一点。如果我添加构建参数-frounding math,我会得到预期的结果。我相应地编辑了答案。2的补码有符号整数有一个特例
-INT_MIN
是有符号的,不是正整数。在这种情况下,编译器可能会假设
a
b
都不是整数,因此这也是一个遗漏的优化。但可能有不同的原因:gcc可能过于谨慎,因为
abs
本身可能会溢出。
fabsprod1(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
fabsprod2(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
g++ -O1 -frounding-math -march=native main2.cpp && ./a.out