下文中理所当然地使用IEEE-754双精度浮点格式。
Python:“...几乎所有机器都使用 IEEE 754 二进制浮点运算,并且几乎所有平台都将 Python 浮点映射到 IEEE 754 二进制 64 位‘双精度’值。”
C++:将
double
与 IEEE-754 双精度相关联。
机器 epsilon 是 st. fl(1+ε)>1.
使用双精度,正式 epsilon = 2^-52,但大多数实现(因为四舍五入到最近的值)为 epsilon = 2^-53。
在 Python (Spyder) 中,当我这样做时:
i = 1.0 + 2**-53
print(i)
>> 1.0
C++版本:
#include <iostream>
#include <cmath>
#include <iomanip>
int main() {
double i = std::pow(2.0, -53);
double j = 1.0 + i;
std::cout << j;
return 0;
}
>> 1.0
此外,当我使用 Python 时:
i = 1.0 + 2**-52 + 2**-53
print(i)
>> 1.0000000000000004
C++版本:
#include <iostream>
#include <cmath>
#include <iomanip>
int main() {
double i = std::pow(2.0, -52);
double j = 1.0 + i;
double k = j + std::pow(2.0, -53);
std::cout << std::setprecision(17) << k;
return 0;
}
>> 1.0000000000000004
其中加法的顺序是从左到右,这样非结合性的魔力就不会发挥作用,即
i = 1.0 + 2**-52 + 2**-53 <=> i = (1.0 + 2**-52) + 2**-53
.
(对我来说)奇怪的事情发生在这里。首先,1 + 2**-52 存储(在寄存器中)作为完全相当于 1 + 2*epsilon 的双精度格式,即
0 + 01111111111 + 000...01
此外,2**-53 存储为:
0 + 01111001010 + 000...00
如果我们把它们写成
(mantissa)*(2**exponent)
的形式,1 + 2**-52
就是(1.0000...01)*(2**0)
,2**-53
就是(1.0000...00)*(2**-53)
,其中尾数(1.xxxx...xx)长度是53(包括隐含的1位) 。按位加法需要相同的指数,因此我移动较小的尾数(即 2**-53),使其具有指数 0:
(1.0000...00)*(2**-53) -> (0.|52 zeros here|10000...00)*(2**0)
所以添加就像:
1.0000...010 (51 zero digits after the radix, then a '1' and '0' making a total of 53 digits)
0.0000...001 (52 zero digits after the radix + '1' digit making a total of 53 digits)
+
------------
1.0000...011 (53 digits after the radix)
现在尾数总共应该是53,但是上面是54,所以应该四舍五入。
我的问题从这里开始:这两种编程语言在我执行
1.0000000000000004
时给出1.0 + 2**-52 + 2**-53
输出的原因是因为实现了tie-to-even规则,以便1.0000...011
四舍五入为1.0000...10
,本质上 1.0000000000000004
高达 16 位精度?还是(完全)是别的东西&我在计算等中犯了错误?
抱歉,如果在这样一个简单的主题上看起来有点矫枉过正或想得太多,但是,它困扰了我好几天,我无法弄清楚原因或无法验证我的想法。任何答案和评论都值得赞赏。
是的,这完全是关于舍入到最接近/偶数。 Python 的
float.hex()
可以直接显示这些位:
>>> 1.0 + 2**-52 + 2**-53
1.0000000000000004
>>> _.hex()
'0x1.0000000000002p+0'
尽管 Python 和 C 与此关系不大:这几乎肯定是 CPU/FPU 如何实现浮点加法的结果(它们几乎肯定默认在硬件中实现 754 风格的最接近/偶数舍入)。 `` 请注意,在此特定示例中,关联性并不重要。
1.0 + 2**-52
和 2**-52 + 2**-53
本身都可以完全表示。
是的,平局。这是默认的 IEEE 754 舍入模式。 IEEE 754 定义了 4 种可能的舍入模式:
选择除默认值之外的任何内容是一项极其专业的操作,大多数语言甚至都不支持。