使用 -O0 和 -O2 编译的两个程序是否应该各自产生相同的浮点结果?

Should two programs compiled with -O0 and -O2 each produce identical floating point results?

提问人:gbg 提问时间:10/21/2022 最后编辑:gbg 更新时间:10/21/2022 访问量:266

问:

简短示例:

#include <iostream>
#include <string_view>
#include <iomanip>

#define PRINTVAR(x) printVar(#x, (x) )

void printVar( const std::string_view name, const float value )
{
    std::cout 
        << std::setw( 16 )
        << name 
        << " = " << std::setw( 12 ) 
        << value << std::endl;
}

int main()
{
    std::cout << std::hexfloat;
    
    const float x = []() -> float
    {
        std::string str;
        std::cin >> str; //to avoid 
                         //trivial optimization
        return strtof(str.c_str(), nullptr);
    }();

    const float a = 0x1.bac178p-5;
    const float b = 0x1.bb7276p-5;

    const float x_1 = (1 - x);

    PRINTVAR( x );
    PRINTVAR( x_1 );
    PRINTVAR( a );
    PRINTVAR( b );

    PRINTVAR( a * x_1 + b * x );
    return 0;
}

Godbolt 上的这段代码

此代码在不同的平台/编译器/优化上产生不同的输出:

X = 0x1.bafb7cp-5 //this is float in the std::hexfloat notation
Y = 0x1.bafb7ep-5

输入值始终相同:0x1.4fab12p-2

编译器 优化 x86_64 aarch64
GCC-12.2 的 -O0 X X
GCC-12.2 的 -O2型 X Y
叮当-14 -O0 X Y
叮当-14 -O2型 X Y

正如我们所看到的,Clang 在相同的架构中给出了 -O0 和 -O2 之间的相同结果,但 GCC 没有。

问题是 - 我们是否应该期望在同一平台上使用 -O0 和 -O2 获得相同的结果?

C++ GCC 浮点 精度 IEEE-754

评论

4赞 user17732522 10/21/2022
@RichardCritten 不可以,符合标准(意味着没有标志之类的)编译器可能不会执行可能影响值的重新排序。-Ofast
2赞 10/21/2022
曾经有一段时间,C 标准对浮点数几乎没有提及。最新的 C 标准是否需要符合 IEEE?这可能很有用。
1赞 Kaz 10/21/2022
@DanielMGessel 曾经有一段时间,C 标准对 iostream、std::cout 和 []() 也很少提及。哦,等等,那是今天。:)
2赞 dewaffled 10/21/2022
关于上面提到的 gcc,文档说它至少在涉及内置时会有所不同——Should -O0 and -O2 produce identical results?However, as library functions may not be correctly rounded for all arguments, function results may differ depending on whether they are evaluated at compile time or at run time.
2赞 Kaz 10/22/2022
@Boann 浮点加法是出了名的非关联。必须保留这样的“细节”,否则该语言不适合数值计算。就此而言,如果操作数是整数,则加法也不能重新排列,除非类型具有可逆溢出(例如 2 的补码环绕)。

答:

7赞 Eric Postpischil 10/21/2022 #1

问题是 - 我们是否应该期望在同一平台上使用 -O0 和 -O2 获得相同的结果?

不,一般不行。

C++ 2020 草案 N4849 7.1 [expr.pre] 6 说:

浮点操作数的值和浮点表达式的结果可以以比类型所需的精度和范围更高的精度和范围表示;因此,类型不会更改。51

脚注 51 说:

强制转换和赋值运算符仍必须按照 7.6.1.3、7.6.3、7.6.1.8 中所述执行其特定转换 和 7.6.19。

这意味着在计算时,C++实现可以使用操作数的名义类型,或者可以使用任何具有更高精度和/或范围的“超集”格式。这可能是 OR 或未命名的格式。完成计算并将结果分配给变量(在您的示例中包括函数参数)后,必须将使用扩展精度计算的结果转换为类型中可表示的值。因此,您将始终看到一个结果,但它可能与完全使用该类型执行算术的结果不同。a * x_1 + b * xfloatdoublelong doublefloatfloatfloat

C++ 标准不要求 C++ 实现对它在所有实例中使用的精度做出相同的选择。即使确实如此,编译器的每个命令行开关组合也可能被视为不同的 C++ 实现(至少对于可能影响程序行为的开关)。因此,获得的 C++ 实现可以通体使用算术,而获得的 C++ 实现可以使用扩展精度。-O0float-O2

请注意,用于计算的扩展精度不仅可以通过使用更广泛类型的机器指令(例如对值而不是值进行操作的指令)获得,还可以通过诸如融合乘加等指令产生,该指令的计算方式就好像以无限精度计算 •+,然后四舍五入到标称类型。这样可以避免先计算生成结果,然后再添加到 中时会发生的舍入错误。doublefloata*b+cabca*bfloatc

评论

1赞 amonakov 10/22/2022
请注意,C 以更好的方式指定它,程序员可以检查宏,例如,如果它的值为 2,则中间值为 ;FMA 收缩可以通过编译指示(理论上)或编译器命令行(实践中)禁用。FLT_EVAL_METHODlong doubleSTDC FP_CONTRACT