与 GCC 的关联数学

associative-math with GCC

我在 C 中创建了一个 double-double 数据类型。我尝试使用 GCC -Ofast 并发现它快得多(例如,使用 -O3 1.5 秒,使用 [= 0.3 秒13=]) 但结果是假的。我追查到 -fassociative-math。我很惊讶这不起作用,因为我在重要时明确定义了我的操作的关联性。例如,在下面的代码中,我在重要的地方加上了括号。

static inline doublefloat two_sum(const float a, const float b) {
        float s = a + b;
        float v = s - a;
        float e = (a - (s - v)) + (b - v);
        return (doublefloat){s, e};
}

所以我不希望 GCC 发生变化,例如(a - (s - v))((a + v) - s) 即使 -fassociative-math。那么,为什么使用 -fassociative-math 结果如此错误(而且速度如此之快)?

我尝试 /fp:fast 使用 MSVC(在将我的代码转换为 C++ 之后),结果是正确的,但它并不比 /fp:precise.

GCC 手册中关于 -fassociative-math 的说明

Allow re-association of operands in series of floating-point operations. This violates the ISO C and C++ language standard by possibly changing computation result. NOTE: re-ordering may change the sign of zero as well as ignore NaNs and inhibit or create underflow or overflow (and thus cannot be used on code that relies on rounding behavior like "(x + 2^52) - 2^52". May also reorder floating-point comparisons and thus may not be used when ordered comparisons are required. This option requires that both -fno-signed-zeros and -fno-trapping-math be in effect. Moreover, it doesn't make much sense with -frounding-math.

编辑:

我对整数(有符号和无符号)和浮点数进行了一些测试,以检查 GCC 是否简化了关联运算。这是我测试的代码

//test1.c
unsigned foosu(unsigned a, unsigned b, unsigned c) { return (a + c) - b; }
signed   fooss(signed   a, signed   b, signed   c) { return (a + c) - b; }
float    foosf(float    a, float    b, float    c) { return (a + c) - b; }
unsigned foomu(unsigned a, unsigned b, unsigned c) { return a*a*a*a*a*a; }
signed   fooms(signed   a, signed   b, signed   c) { return a*a*a*a*a*a; }
float    foomf(float    a, float    b, float    c) { return a*a*a*a*a*a; }

//test2.c
unsigned foosu(unsigned a, unsigned b, unsigned c) { return a - (b - c);     }
signed   fooss(signed   a, signed   b, signed   c) { return a - (b - c);     }
float    foosf(float    a, float    b, float    c) { return a - (b - c);     }
unsigned foomu(unsigned a, unsigned b, unsigned c) { return (a*a*a)*(a*a*a); }
signed   fooms(signed   a, signed   b, signed   c) { return (a*a*a)*(a*a*a); }
float    foomf(float    a, float    b, float    c) { return (a*a*a)*(a*a*a); }

我遵守了 -O3-Ofast 并查看了生成的程序集,这就是我观察到的结果

由此我得出结论

换句话说,GCC 所做的正是我没有预料到的 -fassociative-math。它将 (a - (s - v)) 转换为 ((a + v) - s).

人们可能认为这在 -fassociative-math 中很明显,但在某些情况下,程序员可能希望浮点数在一种情况下是关联的,而在另一种情况下是非关联的。 For example auto-vectorization and reducing a floating point array requires -fassociative-math 但如果这样做,双浮点数就不能在同一个模块中使用。所以唯一的选择是将关联浮点函数放在一个模块中,将非关联浮点函数放在另一个模块中,然后将它们编译成单独的目标文件。

I'm surprised this does not work because I explicitly define the associativity of my operations when it matters. For example in the following code I but parentheses where it matters.

这正是 -fassociative-math 所做的:它忽略您的程序定义的顺序(与没有括号的定义一样),而是执行允许简化的操作。通常,对于 double-double 加法,误差项计算为 0,因为如果浮点运算是关联的,它就等于 0。 e = 0;e = (a - …;快多了,当然,只是错了

在C99标准中,6.5.6:1中的以下语法规则暗示x + y + z只能解析为(x + y) + z:

additive-expression:
         multiplicative-expression
         additive-expression + multiplicative-expression
         additive-expression - multiplicative-expression

对中间左值的显式括号和赋值不会阻止 -fassociative-math 执行其操作。即使没有它们也定义了顺序(在一系列加法和减法的情况下从左到右),并且您告诉编译器忽略定义的顺序。事实上,在应用优化的中间表示上,我怀疑该顺序是由中间赋值、括号或语法强加的信息是否仍然存在。

您可以尝试将所有您希望按照 C 标准强加的顺序编译的函数放在同一个编译单元中,您将在不使用 -fassociative-math 的情况下进行编译,或者在整个程序中完全避免使用此标志.如果你坚持在用 -fassociative-math 编译的编译单元中保留双双加法,你可以尝试使用 volatile 变量,但是 volatile 类型限定符只使对左值的访问成为可观察的事件,它不会强制进行正确的计算。