C语言 二进制 64 浮点加法舍入模式错误和行为差异 32/64 位



当我尝试在英特尔酷睿 I7/I5 上添加以下两个浮点数时,我注意到一个舍入错误:

2.500244140625E+00 + 4503599627370496.00 <=>0x1.4008p+1 + 0x1.0p+52

添加,由faddl汇编指令(当我使用 32 位编译器编译时)使用两个double精度常数进行。

我得到的结果是:

4.503599627370498E+15 = 0x1.0000000000002p+52

而不是:

4.503599627370499E+15 = 0x1.0000000000003p+52

(正如我所期望的那样,并得到了 http://weitz.de/ieee/的证实。

示范:

0x1.0p+52 = 0x10000000000000.00p+0

0x1.4008p+1 = 0x2.801p+0

0x10000000000000.00p+0 + 0x2.801p+0 = 0x10000000000002.801p+0(确切)

0x10000000000002.801p+0 = 0x1.0000000000002801p+52(正好)

0x10000000000002.801p+0 = 0x1.0000000000003p+52(四舍五入后)

我在调试模式下仔细检查并验证我的 FPU 是否处于"舍入到最接近模式"。

更奇怪的是,当我用 64 位编译器编译我的代码,然后使用addsd指令时,没有舍入错误

有没有人可以给我参考或解释关于在同一 FPU 上使用不同指令集的"双重"加法的精度差异?

FPU 寄存器的宽度为 80 位,每当fld加载单精度或双精度数字时,默认情况下会将其转换为双精度扩展精度1
因此fadd通常适用于 80 位数字。

SSE 寄存器与格式无关,SSE 扩展不支持双扩展精度。
例如,addpd使用双精度数字。

默认的舍入模式是四舍五入到最接近(偶数

),这意味着通常的舍入到最接近,但在平局的情况下朝向偶数端(例如 4.5 => 4)。

为了实现 IEEE 754 要求,以无限精度的数字执行算术运算,硬件需要两个保护位和一个粘性位2


我将一个双精度数字写为

<sign> <unbiased exponent in decimal> <implicit integer part> <52-bit mantissa> | <guard bits> <sticky bit>

两个数字

2.500244140625
4503599627370496

+  1 1 0100000000 0010000000 0000000000 0000000000 0000000000 00
+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 00

第一个是移位的

+ 52 0 0000000000 0000000000 0000000000 0000000000 0000000000 10 |10 1   
+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 00 |00 0

总和完成

+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 10 |10 1

四舍五入到最接近(偶数)得到

+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 11

因为0 |10 10 |00 0更接近1 |00 0.

双扩展

这两个数字是

+  1 1 0100000000 0010000000 0000000000 0000000000 0000000000 0000000000 000
+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 000

第一个是转移

+ 52 0 0000000000 0000000000 0000000000 0000000000 0000000000 1010000000 000 | 10 0
+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 000 | 00 0

总和完成

+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 1010000000 000 | 10 0

四舍五入到最接近(偶数):

+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 1010000000 000

由于0 | 10 0被领带打破到最近的偶数。

当这个数字从双倍扩展精度转换为双倍精度(由于fstp QWORD [])时,使用双倍扩展尾数的第52、53和54位作为保护和粘性位重复舍入。

+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 1010000000 000
+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 10|100
+ 52 1 0000000000 0000000000 0000000000 0000000000 0000000000 10

因为0|100再次被领带打破到最近的偶数。


1请参阅英特尔手册 - 第 1 卷的第 8.5.1.2 章。
2保护位是在其中一个数字移动以使指数匹配后保留的额外精度位。粘性位它比最少保护的位不重要。请参阅本页的"四舍五入"部分和 Goldberg 了解格式方法。

感谢我的问题收到的所有评论,我了解发生了什么,并能够解决问题。

我将尝试在这里总结一下。

首先,确认了不正确的四舍五入。正如@MarkDickinson所提到的,这可能是由于"双重四舍五入",但我不知道是否可以确认。事实上,这也可能是由于其他现象造成的,例如Pascal Cuoq给出的出版物中描述的现象。

在对某些数字进行四舍五入的问题时,IA32 FPU 似乎并不完全符合 IEEE754 标准。

默认情况下,GCC(32 位版本)生成的代码使用 FPU 计算 Binary64 数字的加法。

但是,在我的电脑(英特尔酷睿i7)上,SSE单元也能够进行这些计算。默认情况下,GCC(64 位版本)使用此单位。

在GCC32命令行上使用以下两个选项解决了我的问题。

-

msse2 -mfpmath=SSE.

(谢谢EOF)

首先,您正在查看以 10 为基数的数字。 你想谈论浮点和舍入,这需要是一个基数 2 的讨论。

第二个单尾数和双尾数的长度不同,所以很明显,对于相同的数字,您舍入的地方以十进制 1.2345678 变化,我们可以将其四舍五入 1.23 或可以四舍五入 1.2346,具体取决于我们允许的位数向上舍入一个向下舍入,采用舍入规则。

由于您在这里的某个时候是基数 10,因此您还混合了可能的编译时转换、运行时操作和运行时转换

我拿

float x=1.234567;
x=x*2.34;
printf("%fn",x);

有编译时转换,首先是ASCII加倍,然后加倍到浮点数以完全准确地对语言准确(没有将F放在常量的末尾)。 然后运行时乘以,然后运行时转换为 ASCII,运行时 C 库可能与编译时不同,它们是否遵循相同的舍入设置等。 很容易找到你简单地声明 x=1.234 的数字......然后下一行代码是 printf,printf 不是你输入它的东西,除了运行时浮点到 int 之外没有浮点数学。

因此,在您提出此问题之前,我们需要查看您的数字的二进制版本,您的问题的答案几乎应该自动从中消失,而无需进一步的帮助,但是如果您仍然需要帮助,请发布它,我们可以查看它。 基于十进制的讨论会增加编译器和库问题,并且在出现问题时更难隔离问题。

最新更新