如何让 GCC (C++) 停止简化算术表达式?

Posted

技术标签:

【中文标题】如何让 GCC (C++) 停止简化算术表达式?【英文标题】:How do I get GCC (C++) to stop simplifying arithmetic expressions? 【发布时间】:2020-05-15 22:28:48 【问题描述】:

我正在制作一个有趣的物理引擎。 我试图让它即使在低滴答率下也很可靠,所以我在浮点算术和精度上做着危险的舞蹈。

在调试时,我最终运行了这段代码:

#define A 0.2063387632369995100000000000000f
#define B 0.7307806611061096200000000000000f


float a = A;
float b = B;

float floatie1 = A + (B * ((A)/(-B)));
float floatie2 = a + (b * ((a)/(-b)));
printf("%.40f\n", floatie1);
printf("%.40f\n", floatie2);

输出是:

0.0000000149011611938476560000000000000000
0.0000000000000000000000000000000000000000

以及每个(分别)的位:

00110010100000000000000000000000
00000000000000000000000000000000

我知道该表达式的计算结果应该为 0。 我不希望它的计算结果为零,因为我可能在代码中的某处有一些其他不相关的算术,它会给我一个以完全相同的方式不精确的值,如果我然后减去 2,它将是 0。 我不希望我的表达在任意情况下被简化或优化。

我尝试过使浮点数不稳定并关闭优化无济于事。 我也尝试过-f-associative-math-ftrapping-math-funsafe-math-optimizations,以及它们的no- 变体的组合。

如果我分解表达式,将内容放入不同的变量中,一次只做一件事,它会起作用,但我不想每次写一段代码时都背着我。

MSVC++ 使用此代码为我提供了正确的结果。

版本:gcc (MinGW.org GCC-6.3.0-1) 6.3.0 windows 8.1。 如何关闭此功能?

【问题讨论】:

你为什么不为此使用一个理智的函数,比如remainderf(A, B) @EOF 因为这与该表达式(或派生该表达式的原始表达式)应该做什么无关。 这里有几点需要说明。首先,你是为 32 位编译的吗?如果是这样,一个很好的猜测是问题源于可恶的 x87 FPU,默认情况下它在 32 位模式下使用,并且不(完全)符合 ieee754。在为 x86_64 Linux 编译 gcc 时,我无法重现您的问题。您自己编写的不稳定的 remainderf() 会为编译时和运行时计算产生相同(不稳定)的结果,而库 remainderf() 会产生正确的结果。 如果这不是另一个x87脑损伤的受害者,我会感到惊讶。在编译时,gcc 尊重 ieee754,因为为什么不呢,那时它不会花费任何成本。在运行时,x87 FPU 需要 可怕 的性能成本才能符合 ieee754,因为要真正float 精度进行计算,每个中间结果都必须存储到内存中并随后重新加载以继续计算。太疯狂了,编译器通常只在必须时才这样做。您可以通过将每个中间结果存储到变量(提示、提示)或将ffloat-store 传递给gcc 来强制它。 您可以通过一些#pragma push #pragma GCC optimize (STRING, ...) [...] #pragma pop__attribute__((optimize (STRING, ...))) 魔法启用-ffloat-store-deoptimization。 【参考方案1】:

所有功劳归EOF。

我问错问题了。简化表达式并不是真正的 GCC。

显然 GCC 在默认情况下对 32 位进行了优化,使其使用更少的 x87 FPU 指令。尽管速度更快,但并不能真正保证符合 IEEE 754,因为使用了引擎盖下的双精度(EOF 在他的comment 中给出了更详细的解释)。

结果不同的原因是因为 GCC 在编译时使用了符合 IEEE 754 的算法来计算第一个表达式的值,而对于第二个表达式(运行时),它使用了不符合标准的 FPU 指令.

为了避免这些指令(并记住它会使代码变慢),可以使用-ffloat-store 作为编译选项。

或者,如果您不希望整个程序变慢,请使用a pragma 或函数属性:

void __attribute__((optimize ("-ffloat-store"))) func(float a, float b)
    //your code

注意:可以选择尝试定位SSE extensions,而不是x87 FPU,它们既符合IEEE 754 标准,也是x86_64 的基准。更多信息here。

【讨论】:

ffloat-store 不是优化。它严重地去优化代码,以换取硬件上的 ieee754 一致性,该硬件符合有效早于 ieee754 的设计。您可能要注意,您还可以通过针对更新的(现在也很老的)向量/浮点单元(SSE 和更高版本)来获得 ieee754 一致性,它始终是 x86_64 的一部分。 x86_64 的默认编译器目标是符合 ieee754 的标准,并且不会影响性能。 @EOF 已编辑。抱歉,我的想法是正确的,但那时我的睡眠不足。也添加了注释。谢谢! -ffloat-store 不会避免使用一些“更快的指令”,而是使用相同的指令,但更多的指令(​​特别是更多的浮点加载/存储指令)。 @EOF 我很困惑。如果它使用相同的指令,结果怎么会不同? 问题在于 x87 FPU 的设计。 x87 FPU 由一组 8 个 80 位 extended double 浮点寄存器组成。没有办法让 x87 FPU 存储 32 位 float 变量(AFAIR,它总是使用 16 位的扩展指数范围,而不是 floatdouble 的 8 / 11 位)。将中间结果正确转换为符合 ieee754 的float唯一 方法是将值存储到内存中(它 ieee754 格式),然后重新加载该值,所以它不再具有扩展的精度/范围。

以上是关于如何让 GCC (C++) 停止简化算术表达式?的主要内容,如果未能解决你的问题,请参考以下文章

算术 C++ 运算符

C++或C语言如何将字符串转化为数学表达式

在 C 和 C++ 中对齐堆数组以简化编译器 (GCC) 向量化

为啥我的 SFINAE 表达式不再适用于 GCC 8.2?

编译可变参数模板时 GCC 5.3.1 C++ 停止

C++:算术表达式求值