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

gbg*_*gbg 8 c++ floating-point precision gcc ieee-754

简短的例子:

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

Godbolt 上的这段代码

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

X = 0x1.bafb7cp-5 //this is float in the std::hexfloat notation
Y = 0x1.bafb7ep-5
Run Code Online (Sandbox Code Playgroud)

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

编译器 优化 x86_64 架构64
GCC-12.2 -O0 X X
GCC-12.2 -氧气 X
铿锵-14 -O0 X
铿锵-14 -氧气 X

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

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

Eri*_*hil 7

\n

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

\n
\n

不,一般来说不是。

\n

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

\n
\n

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

\n
\n

脚注 51 说:

\n
\n

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

\n
\n

这意味着在计算 时a * x_1 + b * x,C++ 实现可以使用float操作数的标称类型,也可以使用任何具有更高精度和/或范围的 \xe2\x80\x9csuperset\xe2\x80\x9d 格式。这可能是doubleorlong double或 未命名的格式。评估完成并将结果分配给变量(在您的示例中包括函数参数)时,必须将使用扩展精度计算的结果转换为可在类型中表示的值float。因此,您总是会看到float结果,但它可能与完全使用float类型执行算术的结果不同。

\n

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

\n

double请注意,用于计算的扩展精度不仅可以通过使用更广泛类型的机器指令(例如对值而不是值进行操作的指令)来获得,float还可以通过诸如融合乘加之类的指令来获得,该指令计算a*b+c就好像a\xe2\x80\xa2 b+c以无限精度计算,然后舍入到标称类型。a*b这避免了如果首先计算、产生float结果然后添加到 时会发生的舍入误差c

\n