一个大数字如何精确地适合 32 位 GCC 的“double”?

How can a big number fit precisely into `double` with 32-bit GCC?

提问人:yeputons 提问时间:10/15/2023 最后编辑:Jan Schultkeyeputons 更新时间:10/15/2023 访问量:687

问:

请考虑以下代码:

#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 上使用 32 位 GCC 编译时 (),它按如下方式打印g++ -m32 a.cpp

123456789123456784
123456789123456784.000000
0
123456789123456789
1

请注意,根据完成方式,转换到然后转换回的结果会有所不同。如果我通过一个单独的变量(行和 )来做到这一点,结果以 4 结尾。但是如果我在一个表达式(行)中执行所有操作,则结果以 9 结尾,就像原始值一样。long longdoublelong longdouble y(1)(2)(4)

这是非常不方便的:当转换为 时,不存在任何结果,并且签入行确认了这一点。但是,行中的检查就像有一个一样通过。是 GCC 中的错误还是我的程序中的错误?double123456789123456789long long(3)(5)

根据上面的 Godbolt 链接,此行为始于 GCC 9,GCC 8 工作正常。更有趣的是,如果我添加,所有表达式在编译过程中都会被优化,输出为:-O2

123456789123456784
123456789123456784.000000
0
123456789123456784
0

如果我从 read from 并保留 ,中间变量以 结尾,但在转换为 wild 后出现。xstd::cin-O24long long9

123456789123456789
123456789123456784.000000
1
123456789123456789
1

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

  • 从整数类型到浮点类型的转换是定义的:它会产生一个舍入的浮点值。所以要么或.123456789123456784123456789123456790
  • 这两个值都适合 。long long
C++ GCC 浮点 编译器错误

评论


答:

17赞 yeputons 10/15/2023 #1

这似乎是臭名昭著的 GCC(非)bug 323 的另一个实例。有人可能会争辩说它更接近于例如错误 85957,因为问题发生在整数上,并且没有任何浮点数计算。

然而,潜在的问题可能是一样的:GCC 在 32 位模式下使用长双精度进行后台计算,因为这是 8086 的 FPU (8087) 在过去使用的。看拆解:

; 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
    ...

因此,line 实际上将 存储到内存位置并将其截断为 64 位,line 稍后从内存中获取这 64 位。但line直接与80位寄存器配合使用,并精确地适应80位IEEE扩展的双精度数字。因此,没有舍入,经过一些代码后,我们得到了这个确切的值。(1)double(2)(4)123456789123456789long long

令人惊讶的是,即使添加选项也不会改变最新 GCC 13.2 的结果。我以为“使用 SSE 指令而不是 x87”(我相信这是默认设置)会改变一些东西,但事实并非如此。-msse -msse2 -mfpmath=sse -march=skylakeg++ -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++ -m32123456789123456789

123456789123456784
123456789123456784.000000
0
123456789123456789
1

volatile强制 GCC 实际存储并加载到内存中,强制将 80 位值四舍五入为 64 位。不知道这是如何记录的。程序范围的记录选项是 .double-ffloat-store

评论

1赞 Peter Cordes 10/16/2023
相关:请参阅评论中关于 C 和 x86 汇编中自制“fabs”函数的讨论 - OP 结果的有趣部分是,在提升到比 a 更精确之后,就好像它被解析为 .(问题的内联 asm 部分不相关。有趣的是,在那种情况下,给出了我们预期的结果。gcc.gnu.org/bugzilla/show_bug.cgi?id=108742/gcc.gnu.org/bugzilla/show_bug.cgi?id=110476 是最相关的。-189.55fdoublefloat189.55-fexcess-precision=fast
15赞 user17732522 10/15/2023 #2

GCC 确实在 https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html 中记录了这种非标准行为。

如果未给出任何选项(即,如果使用默认值),则设置为 。-std=c++XX-std=gnu++XX-fexcess-precisionfast

这样做的结果是,违反 C 和 C++ 标准,GCC 假设操作始终可以以比类型允许的更高的精度执行,并且未指定在任何给定实例中是否会发生这种情况。

但是,C 和 C++ 标准要求强制转换和赋值四舍五入到实际目标类型中可表示的值。因此,您的检查结果不允许与您描述的那样。(但是,对于单个表达式中的其他运算,即算术运算,标准明确允许以更高的精度进行运算。1

您将获得符合标准的行为,该行为通过选项自动启用。但是,对于 C++,它仅在 GCC 13 之后实现。-fexcess-precision=standard-std=c++XX

类似地,GCC 默认为 ,这也是不合规的,并允许 GCC 跨语句收缩浮点运算,即假设无限精度的中间结果,而标准再次不允许跨语句、强制转换或赋值这样做。符合标准的选项是(或完全禁用它)。-ffp-contract=fast-ffp-contract=on-ffp-contract=off

(但是,这并不意味着仍然没有 bug 使行为不符合,正如另一个答案中链接的 bug 报告中所讨论的那样。

评论

1赞 Peter Cordes 10/16/2023
有趣的事实:GCC 与 ;据推测,内部不支持跟踪乘法和加法是否是同一表达式的一部分,因此避免跨语句收缩的唯一方法是根本不收缩。godbolt.org/z/dqKqvd8f8 显示 GCC 与 clang:clang 完全支持 .-ffp-contract=onoffon