【问题标题】:Javascript floating point number confusionJavascript浮点数混淆
【发布时间】:2014-09-19 02:09:31
【问题描述】:

我遇到了一个有些困惑的操作。

var a = 0.1;
var b = 0.2;
var c = 0.3;

console.log(a); // 0.1
console.log(b); // 0.2
console.log(c); // 0.3

但是,

consolo.log(a+b+c) // 0.6000000000000001.

虽然

console.log(a+(b+c)) // 0.6

我知道 Javascript 使用二进制浮点,因此不能准确表示 0.1、0.2、0.3 但是(b + c)周围的括号是什么。这里有任何转换或四舍五入吗?

非常感谢,

【问题讨论】:

  • 好吧,在第一种情况下,您正在使用(0.1 + 0.2) + 0.3 = 0.3 + 0.3,在第二种情况下,您正在使用0.1 + (0.2 + 0.3) = 0.1 + 0.5。我猜第一种情况的舍入误差比第二种情况大。
  • 我的想法和Felix一样,但是JavaScript的“数字”类型是双精度浮点数。这应该比示例提供更好的精度。 (我在 FF 和 Chrome 中复制了 OP 的示例,还发现 (a+b)+c 给出的结果与 a+b+c 相同,因此问题在于评估顺序,而不是括号的存在。)
  • 是的,我的意思是,既然括号改变了顺序,为什么 (0.2 + 0.3) 上的舍入行为不同于 (0.1+0.2)
  • 受虐狂可以通过将值转换为 IEEE 754 DP 浮点二进制并进行算术来解决这个问题。我想我会等待一位数学家出现。
  • @BobBrown, “...应该比示例提供更好的精度。”该示例具有 16 位精度。这大约是双精度二进制浮点数的精度限制。

标签: javascript rounding data-conversion


【解决方案1】:

如何定义 JavaScript 数字

JavaScript 数字用IEEE754 表示,它是双精度二进制浮点 (binary64),它是科学计数法,以 2 为基数。一个数有64位,分为3部分(从高位到低位):

  • 第一位是符号:0 - 正数; 1 - 否定
  • 接下来的 11 位是指数部分
  • 最后 52 位用于尾数/小数

因此,浮点数计算为:(-1) ^ sign * (2 ^ exponent) * significand

注意:由于科学计数法的指数部分可以是正数或负数,所以二进制64数的实际指数值应通过减去指数偏差(即 11 位指数值的中间值 1023)。

标准中还定义了有效数字值在[1, 2).之间,因为有效数字部分的第一个数字总是1,所以在上图中是隐含的,没有给出。所以,基本上有效数部分实际上是53位精度,上图中红色部分只是尾数或小数部分。

二进制64格式的0.1、0.2和0.3

根据标准,不难找到 binary64 格式的 0.1、0.2 和 0.3(您可以手动计算,也可以通过此工具计算 http://bartaz.github.io/ieee754-visualization/):

0.1

0 01111111011 1001100110011001100110011001100110011001100110011010

科学记数法是

1.1001100110011001100110011001100110011001100110011010 * 2e-4

注意:有效数字部分为二进制格式,以下数字格式相同

0.2

0  01111111100 1001100110011001100110011001100110011001100110011010

科学记数法是

1.1001100110011001100110011001100110011001100110011010 * 2e-3

0.3

0 01111111101 0011001100110011001100110011001100110011001100110011

科学记数法是

1.0011001100110011001100110011001100110011001100110011 * 2e-2

将 2 个 binary64 数字相加的步骤

第 1 步 - 对齐指数

  • 移动具有较小指数的数字的有效数字
  • 有效位右移
  • 对于每个有效位数移位将指数增加 1,直到两个指数相同
  • 移位后,有效数字应向上舍入。

第 2 步 - 将有效数字相加

  • 如果相加后的有效位不满足[1,2) 要求,则将其移至该范围并更改指数

  • 移位后,有效数字应向上取整。

0.1 + 0.2 + 0.3 == 0.6000000000000001

如上所述,0.1 有指数-40.2 有指数-3,所以需要先做指数对齐:

0.1 移出

1.1001100110011001100110011001100110011001100110011010 * 2e-4

0.1100110011001100110011001100110011001100110011001101 * 2e-3

然后加上有效位

0.1100110011001100110011001100110011001100110011001101

1.1001100110011001100110011001100110011001100110011010

我们得到相加的有效值:

10.0110011001100110011001100110011001100110011001100111

但它不在[1,2) 范围内,因此需要将其右移(向上舍入)到:

1.0011001100110011001100110011001100110011001100110100 (* 2e-2)

然后添加到

0.3 (1.0011001100110011001100110011001100110011001100110011 * 2e-2) 

我们得到:

10.0110011001100110011001100110011001100110011001100111 * 2e-2

再次,我们需要移位和四舍五入,最后得到值:

1.0011001100110011001100110011001100110011001100110100 * 2e-1

正是0.6000000000000001(十进制)的值

使用相同的工作流程,您得到计算 0.1 + (0.2 + 0.3)

工具

本网页http://bartaz.github.io/ieee754-visualization/帮助您快速将十进制数转换为binary64格式,您可以使用它来验证计算步骤。

如果您正在处理单精度二进制浮点数,您可以参考这个工具:http://www.h-schmidt.net/FloatConverter/IEEE754.html

【讨论】:

    【解决方案2】:

    一般问题在Is floating point math broken?中描述。

    在其余部分,我将只看两个计算之间的差异。

    来自我的评论:

    好吧,在第一种情况下,您正在做 (0.1 + 0.2) + 0.3 = 0.3 + 0.3,在第二种情况下,您正在做 0.1 + (0.2 + 0.3) = 0.1 + 0.5。我猜第一种情况下的舍入误差大于第二种情况。

    让我们仔细看看这个计算中的实际值:

    var a = 0.1;
    var b = 0.2;
    var c = 0.3;
    
    console.log('          a:', a.toPrecision(21));
    console.log('          b:', b.toPrecision(21));
    console.log('          c:', c.toPrecision(21));
    
    console.log('      a + b:', (a + b).toPrecision(21));
    console.log('      b + c:', (b + c).toPrecision(21));
    
    console.log('  a + b + c:', (a + b + c).toPrecision(21));
    console.log('a + (b + c):', (a + (b + c)).toPrecision(21));

    输出是

              a: 0.100000000000000005551
              b: 0.200000000000000011102
              c: 0.299999999999999988898
    
          a + b: 0.300000000000000044409
          b + c: 0.500000000000000000000
    
      a + b + c: 0.600000000000000088818
    a + (b + c): 0.599999999999999977796 
    

    因此,很明显,两种计算都有舍入误差,但误差是不同的,因为您以不同的顺序执行加法。碰巧a + b + c 产生了更大的错误。


    控制台似乎将数字四舍五入到小数点后 16 位:

    > (a + b + c).toPrecision(16)
    "0.6000000000000001"
    > (a + (b + c)).toPrecision(16)
    "0.6000000000000000"
    

    这就是为什么第二次计算只会输出0.6。如果控制台会四舍五入到小数点后第 17 位,情况会有所不同:

    > (a + b + c).toPrecision(17)
    "0.60000000000000009"
    > (a + (b + c)).toPrecision(17)
    "0.59999999999999998"
    

    【讨论】:

    • 很好的答案,我也做了测试,发现了舍入错误的位置。
    【解决方案3】:

    这不是 JavaScript 的问题,你也会在其他语言中得到类似的惊喜。

    请阅读:What Every Programmer Should Know About Floating-Point Arithmetic

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-13
      相关资源
      最近更新 更多