关联数学与GCC

Z b*_*son 3 c floating-point precision gcc

我在C中创建了一个双倍数据类型.我尝试-Ofast使用GCC并发现它显着更快(例如1.5秒-O3和0.3 秒-Ofast)但结果是假的.我追了这个-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};
}
Run Code Online (Sandbox Code Playgroud)

所以我不希望海湾合作委员会改变(a - (s - v)),((a + v) - s)甚至改变-fassociative-math.那么为什么结果如此错误使用-fassociative-math(并且速度更快)?

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

从GCC手册中了解到-fassociative-math它的状态

允许在一系列浮点运算中重新关联操作数.这可能会改变计算结果,从而违反了ISO C和C++语言标准.注意:重新排序可能会改变零的符号以及忽略NaN并禁止或创建下溢或溢出(因此不能用于依赖于舍入行为的代码,如"(x + 2 ^ 52) - 2 ^ 52" .也可以重新排序浮点比较,因此在需要进行有序比较时可能不会使用.这个选项要求-fno-signed-zeros和-fno-trapping-math都有效.此外,它没有太大的作用.感觉与-frounding-math.

编辑:

我用整数(有符号和无符号)进行了一些测试,并使用float来检查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; }
Run Code Online (Sandbox Code Playgroud)

//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); }
Run Code Online (Sandbox Code Playgroud)

我遵守-O3并且-Ofast看了生成的组件,这就是我观察到的

  • unsigned:加法和乘法的代码相同(减少到三次乘法)
  • 签名:代码在添加时不相同,但用于乘法(减少到三次乘法)
  • float:加法或乘法的代码不相同,-O3-Ofast加法是相同的,只使用三次乘法,乘法几乎相同.

由此我得出结论

  • 如果一个操作是关联的,那么GCC将简化它,但它选择这样a - (b - c)才能成为(a + c) - b.
  • 无符号加法和乘法是关联的
  • 签名加法不是关联的
  • 有符号乘法是关联的
  • a*a*a*a*a*a在使用时,简化为整数和浮点的三次乘法-fassociative-math.
  • -fassociative-math 导致浮点加法和乘法是关联的.

换句话说,GCC正是我没想到的那样-fassociative-math.它转换(a - (s - v))((a + v) - s).

人们可能认为这是显而易见的,-fassociative-math但有些情况下,程序员可能希望浮点在一次情况下是关联的而在另一种情况下是非关联的.例如,自动矢量化和减少浮点数组需要-fassociative-math但是如果这样做,则双浮点不能在同一模块中使用.所以唯一的选择是将关联浮点函数放在一个模块中,将非关联浮点函数放在另一个模块中,然后将它们编译成单独的目标文件.

Pas*_*uoq 7

我很惊讶这不起作用,因为我明确定义了操作的关联性.例如,在下面的代码中,我将括号括起来.

这正是-fassociative-math它所做的:它忽略了程序定义的顺序(正如没有圆括号所定义的那样)并且做了允许简化的内容.通常,对于双倍加法,误差项计算为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类型限定符只能使左值访问一个可观察的事件,它不会强制进行正确的计算.