提问人:yeputons 提问时间:10/15/2023 最后编辑:Jan Schultkeyeputons 更新时间:10/15/2023 访问量:687
一个大数字如何精确地适合 32 位 GCC 的“double”?
How can a big number fit precisely into `double` with 32-bit GCC?
问:
请考虑以下代码:
#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 long
double
long long
double y
(1)
(2)
(4)
这是非常不方便的:当转换为 时,不存在任何结果,并且签入行确认了这一点。但是,行中的检查就像有一个一样通过。是 GCC 中的错误还是我的程序中的错误?double
123456789123456789
long long
(3)
(5)
根据上面的 Godbolt 链接,此行为始于 GCC 9,GCC 8 工作正常。更有趣的是,如果我添加,所有表达式在编译过程中都会被优化,输出为:-O2
123456789123456784
123456789123456784.000000
0
123456789123456784
0
如果我从 read from 并保留 ,中间变量以 结尾,但在转换为 wild 后出现。x
std::cin
-O2
4
long long
9
123456789123456789
123456789123456784.000000
1
123456789123456789
1
我相信上面的程序中没有未定义的行为:
- 从整数类型到浮点类型的转换是定义的:它会产生一个舍入的浮点值。所以要么或.
123456789123456784
123456789123456790
- 这两个值都适合 。
long long
答:
这似乎是臭名昭著的 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)
123456789123456789
long long
令人惊讶的是,即使添加选项也不会改变最新 GCC 13.2 的结果。我以为“使用 SSE 指令而不是 x87”(我相信这是默认设置)会改变一些东西,但事实并非如此。-msse -msse2 -mfpmath=sse -march=skylake
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
123456789123456784
123456789123456784.000000
0
123456789123456789
1
volatile
强制 GCC 实际存储并加载到内存中,强制将 80 位值四舍五入为 64 位。不知道这是如何记录的。程序范围的记录选项是 .double
-ffloat-store
评论
-189.55f
double
float
189.55
-fexcess-precision=fast
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
您将获得符合标准的行为,该行为通过选项自动启用。但是,对于 C++,它仅在 GCC 13 之后实现。-fexcess-precision=standard
-std=c++XX
类似地,GCC 默认为 ,这也是不合规的,并允许 GCC 跨语句收缩浮点运算,即假设无限精度的中间结果,而标准再次不允许跨语句、强制转换或赋值这样做。符合标准的选项是(或完全禁用它)。-ffp-contract=fast
-ffp-contract=on
-ffp-contract=off
(但是,这并不意味着仍然没有 bug 使行为不符合,正如另一个答案中链接的 bug 报告中所讨论的那样。
评论
-ffp-contract=on
off
on
评论