为啥这种显式转换的结果与隐式转换的结果不同?

Posted

技术标签:

【中文标题】为啥这种显式转换的结果与隐式转换的结果不同?【英文标题】:Why is the result of this explicit cast different from the implicit one?为什么这种显式转换的结果与隐式转换的结果不同? 【发布时间】:2010-10-19 15:49:32 【问题描述】:

为什么这种显式转换的结果与隐式转换的结果不同?

#include <stdio.h>

double  a;
double  b;
double  c;

long    d;

double    e;

int main() 
    a = 1.0;
    b = 2.0;
    c = .1;

    d = (b - a + c) / c;
    printf("%li\n", d);        //    10

    e = (b - a + c) / c;
    d = (long) e;
    printf("%li\n", d);        //    11
    

如果我这样做 d = (long) ((b - a + c) / c);我也得到了 10。为什么分配给替身会有所不同?

【问题讨论】:

它们在我的系统上是一样的(都是 11 个)吗? 你用什么编译这个? 只是为了好玩,尝试将 e 设为局部变量,看看是否会改变。 @Joseph - gcc 4.3.2 @Jon - 没有区别 Dennis:这里是 gcc 4.3.3,不过是 64 位的。 【参考方案1】:

我怀疑区别在于从 80 位浮点值到 long 的转换与从 80 位浮点值到 64 位的转换以及然后到的转换很长。

(出现 80 位的原因是,这是用于实际算术的典型精度,以及浮点寄存器的宽度。)

假设 80 位结果类似于 10.999999999999999 - 从它到 long 的转换产生 10。但是,最接近 80 位值的 64 位浮点值实际上是 11.0,所以两阶段转换最终产生 11。

编辑:给这个更多的重量......

这是一个使用任意精度算术进行相同计算的 Java 程序。请注意,它将最接近 0.1 的双精度值转换为 BigDecimal - 该值为 0.1000000000000000055511151231257827021181583404541015625。 (换句话说,计算的确切结果无论如何不是 11。)

import java.math.*;

public class Test

    public static void main(String[] args)
    
        BigDecimal c = new BigDecimal(0.1d);        
        BigDecimal a = new BigDecimal(1d);
        BigDecimal b = new BigDecimal(2d);

        BigDecimal result = b.subtract(a)
                             .add(c)
                             .divide(c, 40, RoundingMode.FLOOR);
        System.out.println(result);
    

结果如下:

10.9999999999999994448884876874217606030632

换句话说,大约 40 位十进制数字是正确的(远远超过 64 位或 80 位浮点可以处理的数量)。

现在,让我们考虑一下这个数字在二进制中的样子。我没有任何工具可以轻松进行转换,但我们可以再次使用 Java 来提供帮助。假设一个标准化的数字,“10”部分最终使用三位(11 比 1 = 1011)。剩下 60 位尾数用于扩展精度(80 位)和 48 位用于双精度(64 位)。

那么,每个精度中最接近 11 的数字是多少?再次,让我们使用 Java:

import java.math.*;

public class Test

    public static void main(String[] args)
    
        BigDecimal half = new BigDecimal("0.5");        
        BigDecimal eleven = new BigDecimal(11);

        System.out.println(eleven.subtract(half.pow(60)));
        System.out.println(eleven.subtract(half.pow(48)));        
    

结果:

10.999999999999999999132638262011596452794037759304046630859375
10.999999999999996447286321199499070644378662109375

所以,我们得到的三个数字是:

Correct value: 10.999999999999999444888487687421760603063...
11-2^(-60): 10.999999999999999999132638262011596452794037759304046630859375
11-2^(-48): 10.999999999999996447286321199499070644378662109375

现在为每个精度计算出最接近正确值的值 - 对于扩展精度,它小于 11。将这些值中的每一个四舍五入为 long,最终分别得到 10 和 11。

希望这是足以说服怀疑者的证据;)

【讨论】:

这是一个有根据的猜测,在 C# 中看到了类似的效果。顺便说一句,它将取决于处理器和编译器。我 100% 确定这是怎么回事吗?不。我认为这是一个很可能的解释吗?绝对地。比“在我的机器上工作”IMO 更有用。 babbage.cs.qc.edu/IEEE-754 对这类事情很有帮助,虽然它只有 32 位和 64 位计算器,没有 80 位计算器。 @Adam:非常感谢您提供的链接。确实有用。如果最终的“十进制”值是由最接近的双精度表示的 exact 值,那将很有用。 x87 还没有死,但所有浮点寄存器仍然不是 80 位宽。即使在 x87 上,也不一定要在 80 位大小上进行计算。我希望某些编译器使用与 64 位格式相同大小的尾数。你有这方面的信息吗? 有人知道标准对此有何规定吗?似乎这是一个很好的陷阱,即使您正在注意通常的舍入/截断问题,也无法预料到。它似乎也依赖于编译器/过程。我对 doc JP 参考文献的阅读使我相信...【参考方案2】:

我在运行 gcc 4.3.2 的 32 位 x86 linux 系统上也得到 10 和 11。

相关的 C/asm 在这里:

26:foo.c         ****     d = (b - a + c) / c;                                               
  42                            .loc 1 26 0
  43 0031 DD050000              fldl    b
  43      0000
  44 0037 DD050000              fldl    a
  44      0000
  45 003d DEE9                  fsubrp  %st, %st(1)
  46 003f DD050000              fldl    c
  46      0000
  47 0045 DEC1                  faddp   %st, %st(1)
  48 0047 DD050000              fldl    c
  48      0000
  49 004d DEF9                  fdivrp  %st, %st(1)
  50 004f D97DFA                fnstcw  -6(%ebp)
  51 0052 0FB745FA              movzwl  -6(%ebp), %eax
  52 0056 B40C                  movb    $12, %ah
  53 0058 668945F8              movw    %ax, -8(%ebp)
  54 005c D96DF8                fldcw   -8(%ebp)
  55 005f DB5DF4                fistpl  -12(%ebp)
  56 0062 D96DFA                fldcw   -6(%ebp)
  57 0065 8B45F4                movl    -12(%ebp), %eax
  58 0068 A3000000              movl    %eax, d
  58      00
  27:foo.c         ****
  28:foo.c         ****     printf("%li\n", d);                                                
  59                            .loc 1 28 0
  60 006d A1000000              movl    d, %eax
  60      00
  61 0072 89442404              movl    %eax, 4(%esp)
  62 0076 C7042400              movl    $.LC3, (%esp)
  62      000000
  63 007d E8FCFFFF              call    printf
  63      FF
  29:foo.c         ****     //    10                                                           
  30:foo.c         ****
  31:foo.c         ****     e = (b - a + c) / c;                                               
  64                            .loc 1 31 0
  65 0082 DD050000              fldl    b
  65      0000
  66 0088 DD050000              fldl    a
  66      0000
  67 008e DEE9                  fsubrp  %st, %st(1)
  68 0090 DD050000              fldl    c
  68      0000
  69 0096 DEC1                  faddp   %st, %st(1)
  70 0098 DD050000              fldl    c
  70      0000
  71 009e DEF9                  fdivrp  %st, %st(1)
  72 00a0 DD1D0000              fstpl   e
  72      0000
  32:foo.c         ****
  33:foo.c         ****     d = (long) e;                                                      
  73                            .loc 1 33 0
  74 00a6 DD050000              fldl    e
  74      0000
  75 00ac D97DFA                fnstcw  -6(%ebp)
  76 00af 0FB745FA              movzwl  -6(%ebp), %eax
  77 00b3 B40C                  movb    $12, %ah
  78 00b5 668945F8              movw    %ax, -8(%ebp)
  79 00b9 D96DF8                fldcw   -8(%ebp)
  80 00bc DB5DF4                fistpl  -12(%ebp)
  81 00bf D96DFA                fldcw   -6(%ebp)
  82 00c2 8B45F4                movl    -12(%ebp), %eax
  83 00c5 A3000000              movl    %eax, d
  83      00

答案留给感兴趣的读者作为练习。

【讨论】:

【参考方案3】:

Here is a bunch of detail on floating point issues and a really good article. 但基本上,并非所有浮点值都可以用一定数量的位(32 位或 64 位或其他)表示。这是一个深奥的主题,但我喜欢它,因为它让我想起了Prof. Kahan。 :)

【讨论】:

【参考方案4】:

codepad.org (gcc 4.1.2) 反转了您的示例的结果,而在我的本地系统 (gcc 4.3.2) 上,两种情况下我都得到 11。这向我表明这是一个浮点问题。或者,它理论上可以截断 (b - a + c),在整数上下文中将计算为 (2 - 1 + 0) / .1,即 10,而在浮点上下文中 (2.0 - 1.0 + 0.1) ) / .1 = 1.1 / .1 = 11。不过那会很奇怪。

【讨论】:

c 的值不是从 0.1 开始的。它只是最接近 0.1 的两倍。【参考方案5】:

在 Linux 上直接复制/粘贴和编译都给了我 11。添加 d = (long) ((b - a + c) / c); 也会得到 11。OpenBSD 也是如此。

【讨论】:

操作系统不太重要。编译器 + 选项 + 处理器的相关性要高得多。

以上是关于为啥这种显式转换的结果与隐式转换的结果不同?的主要内容,如果未能解决你的问题,请参考以下文章

显式转换与隐式转换

为啥 Linq Cast<> 助手不能与隐式转换运算符一起使用?

这种 Nullable<T> 行为与隐式转换运算符的理由是啥

C语言 显式 隐式是啥意思

显式主包与隐式主包在哪些方面表现不同?

显式Intent与隐式Intent的使用