std::fabs(a * b) 和 std::fabs(a) * std::fabs(b) 之间的区别

Difference between std::fabs(a * b) and std::fabs(a) * std::fabs(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));
}

这里是 godbolt 中令人困惑的编译器输出,其中包含 gcc-10.1(撰写本文时的当前稳定版本)和 -O3:https://godbolt.org/z/ZEFPgF

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

正如@Scheff 在评论中指出的那样,double 和 float 不是实数。但我也没有看到使用 float 类型的极端情况,例如将 Infinity 或 NaN 作为参数,可能会产生不同的输出。

积分的类似问题 (int) :

#include<cmath>

int fabsprod1(int a, int b) {
    return std::abs(a*b);
}
int fabsprod2(int a, int b) {
    return std::abs(a) * std::abs(b);
}
int fabsprod3(int a, int b) {
    return std::abs(std::abs(a) * std::abs(b));
}

结果(使用您的选项 -O3 -std=c++2a -march=cannonlake):

fabsprod1(int, int):
        mov     eax, edi
        imul    eax, esi
        cdq
        xor     eax, edx
        sub     eax, edx
        ret
fabsprod2(int, int):
        mov     eax, edi
        cdq
        xor     eax, edx
        sub     eax, edx
        mov     edx, esi
        sar     edx, 31
        xor     esi, edx
        sub     esi, edx
        imul    eax, esi
        ret
fabsprod3(int, int):
        mov     eax, edi
        cdq
        xor     eax, edx
        sub     eax, edx
        mov     edx, esi
        sar     edx, 31
        xor     esi, edx
        sub     esi, edx
        imul    eax, esi
        ret

https://godbolt.org/z/tf3nZN

这与您关于 "real/floating point" 个数字的陈述相矛盾。

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

我相信我找到了一个反例。我 post 这是一个单独的答案,因为我认为这与整数的情况完全不同。

在我考虑的情况下,我错过了可以更改浮点运算的舍入模式。有问题的是,当他(我猜)在编译时优化 "known" 数量时,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
}

输出不同,取决于我是否使用

编译
g++ -DCIN -O1 -march=native main2.cpp && ./a.out

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

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

使用 -DCIN 时输出为

fabsprod1(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333334p-2
fabsprod2(0x1.999999999999ap-4,-0x1.8p+1): 0x1.3333333333333p-2

没有-DCIN 输出是

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)。