如何在32位GCC中将一个很大的数字精确地放入“double”?

busg9geu  于 2023-10-19  发布在  其他
关注(0)|答案(2)|浏览(106)

请考虑以下代码:

#include <iostream>
int main() {
    long long x = 123456789123456789;
    std::cout << std::fixed;
    auto y = static_cast<double>(x);  // (1)
    std::cout << static_cast<long long>(y) << "\n";  // (2)
    std::cout << y << "\n";
    std::cout << (x == static_cast<long long>(y)) << "\n";  // (3)
    std::cout << static_cast<long long>(static_cast<double>(x)) << "\n";  // (4)
    std::cout << (x == static_cast<long long>(static_cast<double>(x))) << "\n";  // (5)
}

在Linux(g++ -m32 a.cpp)上使用32位GCC编译时,it prints as follows

123456789123456784
123456789123456784.000000
0
123456789123456789
1

请注意,将long long转换为double,然后再转换回long long的结果是不同的,这取决于它是如何完成的。如果我通过一个单独的变量double y(1)(2)行)来执行,结果以4结尾。但是如果我在一个表达式中做所有的事情(行(4)),结果以9结束,就像原始值一样。
这是相当不方便的:不存在当转换为long long时导致123456789123456789double,并且行(3)中的检查确认了这一点。但是,行(5)中的检查通过,就像有一个一样。这是GCC中的bug还是我的程序?
根据上面的Godbolt链接,这种行为始于GCC 9,GCC 8工作正常。更有趣的是,如果我添加-O2,所有表达式在编译过程中都会被优化,输出结果是:

123456789123456784
123456789123456784.000000
0
123456789123456784
0

如果我从std::cin读取x并保留-O2,则中间变量以4结束,但在转换为long long后,会出现一个野生9

123456789123456789
123456789123456784.000000
1
123456789123456789
1

我相信上面的程序中没有未定义的行为:

  • 从整数类型到浮点类型的转换是定义的:它会产生舍入的浮点值。所以要么是123456789123456784要么是123456789123456790
  • 这两个值都适合long long
wgmfuz8q

wgmfuz8q1#

这似乎是另一个臭名昭著的GCC (non-)bug 323的示例。有人可能会说,它更接近于例如。bug 85957,因为这个问题发生在整数上,而没有任何浮点数的计算。
然而,潜在的问题可能是相同的:GCC在32位模式下使用long double进行计算,因为这是8086的FPU(8087)在过去使用的。查看at the disassembly

; auto y = static_cast<double>(x);  // (1)
    ; Load `x` from address `ebp-16` into `ST(0)`, the 80-bit top of the FPU's stack:
    fild    QWORD PTR [ebp-16]
    ; Load the top of the stack into `y` at `ebp-24`, truncated from 80 bits to 64 bits
    fstp    QWORD PTR [ebp-24]
; std::cout << static_cast<long long>(y) << "\n";  // (2)
    ; Load `y` from `ebp-24` into `ST(0)`, that's 64 bits value already
    fld     QWORD PTR [ebp-24]
    ...
; std::cout << static_cast<long long>(static_cast<double>(x)) << "\n";  // (4)
    ; Load `x` from address `ebp-16` into `ST(0)`, the 80-bit top of the FPU's stack:
    fild    QWORD PTR [ebp-16]
    ; Work with `ST(0)` directly, that's 80-bit
    ...

因此,(1)行实际上将double存储到内存位置并将其截断为64位,(2)行稍后从内存中获取这64位。但是(4)行直接与80位寄存器一起工作,123456789123456789精确地适合80位IEEE扩展双精度数。因此,没有舍入,经过一段代码后,我们在long long中得到了这个精确的值。
令人惊讶的是,即使添加-msse -msse2 -mfpmath=sse -march=skylake选项也不会改变最新GCC 13.2的结果。我以为“使用SSE指令而不是x87”(我相信这是g++ -m64的默认设置)会改变一些东西,但事实并非如此。
如果截断到64位很重要,我建议使用一个中间变量volatile double y

#include <iostream>
int main() {
    long long x;
    std::cin >> x;
    std::cout << std::fixed;
    volatile auto y = static_cast<double>(x);  // (1)
    std::cout << static_cast<long long>(y) << "\n";  // (2)
    std::cout << y << "\n";
    std::cout << (x == static_cast<long long>(y)) << "\n";  // (3)
    std::cout << static_cast<long long>(static_cast<double>(x)) << "\n";  // (4)
    std::cout << (x == static_cast<long long>(static_cast<double>(x))) << "\n";  // (5)
}

当使用g++ -m32编译并将123456789123456789作为输入it prints时:

123456789123456784
123456789123456784.000000
0
123456789123456789
1

volatile强制GCC将double实际存储和加载到内存中,强制将80位值舍入为64位。不知道这是怎么记录的。一个程序范围的文档选项是-ffloat-store

pw9qyyiw

pw9qyyiw2#

GCC在https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html中记录了这种非标准行为。
当没有给出-std=c++XX选项时(即,如果使用默认-std=gnu++XX),则-fexcess-precision被设置为fast
这样做的效果是,违反了C和C标准,GCC假设操作总是可以以比类型所允许的更高的精度执行,并且在任何给定示例中是否会发生这种情况是未指定的。
然而,C和C
标准要求强制转换和赋值,以舍入到实际目标类型中可表示的值。因此,您的检查结果不允许是您描述的1。(另一方面,即。算术,在单个表达式中的操作标准 do 明确允许更高精度的操作。
使用-std=c++XX选项自动启用的-fexcess-precision=standard可以获得符合标准的行为。然而,对于C++,它只是在GCC 13之后才实现的。
类似地,GCC默认为-ffp-contract=fast,这也是不一致的,并允许GCC跨语句收缩浮点操作,即。假设无限精确的中间结果,而标准同样不允许跨语句、强制转换或赋值这样做。符合标准的选项是-ffp-contract=on(或完全禁用它的-ffp-contract=off)。
(This但是,这并不意味着没有bug会导致行为不符合要求,正如在另一个答案中链接的bug报告中所讨论的那样。)

相关问题