岁月如歌

用开放的心态,打造专业的人生。

JavaScript 中小数和大整数的精度丢失

with 29 comments

先来看两个问题:

0.1 + 0.2 == 0.3; // false
9999999999999999 == 10000000000000000; // true

第一个问题是小数的精度问题,在业界不少博客里已有讨论。第二个问题,去年公司有个系统的数据库在做数据订正时,发现有部分数据重复的诡异现象。本文将从规范出发,对上面的问题做个小结。

最大整数

JavaScript 中的数字是用 IEEE 754 双精度 64 位浮点数 来存储的,其格式为:

s x m x 2^e

s 是符号位,表示正负。 m 是尾数,有 52 bits. e 是指数,有 11 bits. 在 ECMAScript 规范 里有给出 e 的范围为 [-1074, 971]. 这样,很容易推导出 JavaScript 能表示的最大整数为:

1 x (2^53 - 1) x 2^971 = 1.7976931348623157e+308

这个值正是 Number.MAX_VALUE

同理可推导出 Number.MIN_VALUE 的值为:

1 x 1 x 2^(-1074) = 5e-324

注意 MIN_VALUE 表示最接近 0 的正数,而不是最小的数。最小的数是 -Number.MAX_VALUE

小数的精度丢失

JavaScript 的数字都是双精度浮点数,在计算机里用二进制存储。当有效位数超过 52 位时,会存在精度丢失。比如:

十进制 0.1 的二进制为 0.0 0011 0011 0011 … (循环 0011)
十进制 0.2 的二进制为 0.0011 0011 0011 … (循环 0011)

0.1 + 0.2 相加可表示为:
   e = -4; m = 1.10011001100...1100(52 位)
 + e = -3; m = 1.10011001100...1100(52 位)
---------------------------------------------
   e = -3; m = 0.11001100110...0110
 + e = -3; m = 1.10011001100...1100
---------------------------------------------
   e = -3; m = 10.01100110011...001
---------------------------------------------
 = 0.01001100110011...001
 = 0.30000000000000004(十进制)

根据上面的演算,还可以得出一个结论:当十进制小数的二进制表示的有限数字不超过 52 位时,在 JavaScript 里是可以精确存储的。比如:

0.05 + 0.005 == 0.055 // true

进一步的规律,比如:

0.05 + 0.2 == 0.25 // true
0.05 + 0.9 == 0.95 // false

需要考虑 IEEE 754 的 Rounding modes, 有兴趣的可进一步研究。

大整数的精度丢失

这个问题鲜有人提及。首先得弄清楚问题是什么:

1. JavaScript 能存储的最大整数是什么?
该问题前面已回答,是 Number.MAX_VALUE, 非常大的一个数。

2. JavaScript 能存储的且不丢失精度的最大整数是什么?
根据 s x m x 2^e, 符号位取正,52 位尾数全填充 1, 指数 e 取最大值 971, 显然,答案依旧是 Number.MAX_VALUE.

我们的问题究竟是什么呢?回到起始代码:

9999999999999999 == 10000000000000000; // true

很明显,16 个 9 还远远小于 308 个 10. 这个问题与 MAX_VALUE 没什么关系,还得归属到尾数 m 只有 52 位上来。

可以用代码来描述:

var x = 1; // 为了减少运算量,初始值可以设大一点,比如 Math.pow(2, 53) - 10
while(x != x + 1) x++;
// x = 9007199254740992 即 2^53

也就是说,当 x 小于等于 2^53 时,可以确保 x 的精度不会丢失。当 x 大于 2^53 时,x 的精度有可能会丢失。比如:

x 为 2^53 + 1 时,其二进制表示为:
10000000000...001 (中间共有 52 个 0)

用双精度浮点数存储时:
e = 1; m = 10000..00(共 52 个 0,其中 1 是 hidden bit)

显然,这和 2^53 的存储是一样的。

按照上面的思路可以推出,对于 2^53 + 2, 其二进制为 100000…0010(中间 51 个 0),也是可以精确存储的。

规律:当 x 大于 2^53 且二进制有效位数大于 53 位时,就会存在精度丢失。这和小数的精度丢失本质上是一样的。

hidden bit 可参考:A tutorial about Java double type.

小结

小数和大整数的精度丢失,并不仅仅在 JavaScript 中存在。严格来说,使用了IEEE 754 浮点数格式来存储浮点类型的任何编程语言(C/C++/C#/Java 等等)都存在精度丢失问题。在 C#、Java 中,提供了 Decimal、BigDecimal 封装类来进行相应的处理,才避开了精度丢失。

注:ECMAScript 规范中,已有  decimal proposal,但目前尚未被正式采纳。

最后考考大家:

Number.MAX_VALUE + 1 == Number.MAX_VALUE;
Number.MAX_VALUE + 2 == Number.MAX_VALUE;
...
Number.MAX_VALUE + x == Number.MAX_VALUE;
Number.MAX_VALUE + x + 1 == Infinity;
...
Number.MAX_VALUE + Number.MAX_VALUE == Infinity;

// 问题:
// 1. x 的值是什么?
// 2. Infinity - Number.MAX_VALUE == x + 1; 是 true 还是 false ?

参考资料

Written by lifesinger

March 7, 2011 at 00:07

Posted in Articles

29 Responses

Subscribe to comments with RSS.

  1. 我勒个去了,前些日子还在想Javascript的字符窜是UTF16,最大是FFFF呢,String.fromCharCode(Math.pow(2,32)+65) === String.fromCharCode(Math.pow(2,16)+65) ,就没往下深入了。看完后很惭愧,继续深入。

    mashihua

    March 7, 2011 at 10:49

  2. 恩,这跟大学数学一样啊。。。明明对的他说错,错的他却说对。。。

    Hex Bolts

    March 8, 2011 at 15:22

  3. 太难懂了

    ball mill

    March 9, 2011 at 11:07

  4. 对于JavaScript我还没研究的这么深入

    power cord

    March 9, 2011 at 16:16

  5. 在平常的业务逻辑中应用这些计算,如何能做到精确呢。

    Denis

    March 13, 2011 at 13:04

  6. @Denis: 涉及金额等精度要求很高的计算,尽量在后端计算好,不要依赖 js.

    lifesinger

    March 13, 2011 at 20:23

  7. 感觉kissy越来越大,越来越全,这样和jq相比基本没有优势了。
    而且组件还很少。。。 虽然有几个精品。
    跟我想象中的简洁小巧,适合用于互联网网站的小组件集合框架越来越远。

    hihihi

    March 14, 2011 at 22:23

  8. […] JavaScript 中小数和大整数的精度丢失 […]

  9. 最后考考大家的代码里 “Number” 拼错了。

    题目很有意思。

    1. x 的值是什么?
    考虑大整数的精度丢失,其实不存在这样一个整数。
    因为当 x > 2^53 时,在计算机64位浮点数表示中,x == x + 1。
    当然大家都明白玉伯的意思。
    Number.MAX_VALUE + (Math.pow(2, 970) == Infinity;
    Number.MAX_VALUE + (Math.pow(2, 970) – Math.pow(2, (970 – 53))) == Number.MAX_VALUE;
    Math.pow(2, 970) == Math.pow(2, 970) – Math.pow(2, (970 – 54));
    综上,可以认为玉伯想要的 x = Math.pow(2, 970) – Math.pow(2, (970 – 54))
    PS: 为什么不是971和52, 提示是浮点数的舍入规则。详细可以参考玉伯文中提到的“IEEE 754 双精度 64 位浮点数”。

    2. Infinity – Number.MAX_VALUE == x + 1
    false
    这里其实不用考虑 x 的具体值。
    可以断言 x < Number.MAX_VALUE。而 Infinity – Number.MAX_VALUE == Infinity;

    考虑不周,请多指教。

    zeratul1986

    March 31, 2011 at 21:46

    • 前面 x = Math.pow(2, 970) – Math.pow(2, (970 – 54)) 的答案失误了。
      应该是 x = Math.pow(2, 970) – Math.pow(2, (970 – 53))。
      这样才有
      Number.MAX_VALUE + x == Numer.MAX_VALUE;

      zeratul1986

      March 31, 2011 at 21:57

  10. @zeratul1986: 终于有人思考最后这个题目了,赞

    我的想法是这样的:

    Number.MAX_VALUE.toString(16) = ”
    fffffffffffff800000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000″

    前面有 13 个 f, 二进制就是 52 个 1
    还有一个 8, 二进制是 1000
    也就是说,前面 53 位都是 1

    这样,当 Number.MAX_VALUE + 1 时,1 替代最后一个 0,但 IEEE 754 双精度浮点数的 m 最大为 53(含隐藏位),因此添加的 1 在存储时会被舍弃掉,所以:

    Number.MAX_VALUE + 1 == Number.MAX_VALUE

    同理类推,当 8(1000) 变成 b(1011),b 后面的位取最大值时,依旧有:

    0xfffffffffffffbfffffffffffffffffffffffffffffffffffff
    fffffffffffffffffffffffffffffffffffffffffffffffffffffff
    fffffffffffffffffffffffffffffffffffffffffffffffffffffff
    fffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffffffffffffffffffff == Number.MAX_VALUE

    进一步,当 再增 1, b 变成 c 时,将发生质变:

    0xfffffffffffffc00000000000000000000000000
    000000000000000000000000000000000000
    000000000000000000000000000000000000
    000000000000000000000000000000000000
    000000000000000000000000000000000000
    000000000000000000000000000000000000
    000000000000000000000000000000000000 == Infinity

    这是因为前面将有 54 个连续的 1, 在存储时,exponent 将由
    971 变成 972, 超出了 IEEE 754 双精度浮点数存储格式中 e 的
    最大值,因此质变为 Infinity 了。

    这样,题目中 x 的值就很容易得到了:

    x = 0xfffffffffffffbffff… – 0xfffffffffffff80000…
    = 0x00000000000003ffff…

    注意这个数在IEEE 754 双精度浮点数格式下无法精确存储。

    还能得到两个有趣的结论:

    1. Number.MAX_VALUE 不是一个数,而是一个区间 [0xfffffffffffff80000…, 0xfffffffffffffc0000…)
    2. Infinity 指的是,所有大于等于 0xfffffffffffffc0000… 的数。

    lifesinger

    April 2, 2011 at 08:38

    • 再推广下,只考虑整数,可不可以认为大于 2^53 的浮点数都是表示一个区间,没有丢失精度的数只是区间中的一个数。

      或者考虑小数,可不可以认为所有的浮点数都是表示一个区间,绝对值越大的浮点数表示的区间也越大,绝对值越小的浮点数表示的区间也越小。

      zeratul1986

      April 2, 2011 at 14:34

      • 嗯,应该可以这么延伸。想起一句话:“我不是一个数,是一堆数⋯⋯”

        lifesinger

        April 2, 2011 at 14:40

  11. 关于舍入规则,可以参考这篇博客:浮点数解惑

    lifesinger

    April 2, 2011 at 12:00

  12. 谢谢玉伯的说明。
    仅仅从和的存储舍入考虑很清晰。

    我是这样考虑的。
    Number.MAX_VALUE
    64位浮点数表示为
    “7fef ffff ffff ffff”

    加法运算使其溢出,
    最小的64位浮点数表示为
    “7fd0 0000 0000 0000”
    对应的10进制数为
    2^970 = 9.9792015476736e+291

    然后考虑大整数的精度丢失,在区间
    (2^970-2^(970-54) , 2^970+2^(970-53))
    内的整数,64位浮点数表示都是
    “7fd0 0000 0000 0000”

    因为从双精度浮点数运算考虑,我认为x也是双精度浮点数。
    用javascript代码运行结果来验证的话,
    还是没有使 Number.MAX_VALUE + x + 1 == Infinity 为真的 x 存在的。
    玉伯以为如何?

    zeratul1986

    April 2, 2011 at 13:49

  13. @zeratul1986:

    Number.MAX_VALUE 的 64 位浮点数表示是:

    0xfffffffffffff800000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000000000000000000
    00000000000000000000000

    二进制就是前面有 53 个 1

    你说的 “7fef ffff ffff ffff” 是怎么得到的?我觉得不对。

    lifesinger

    April 2, 2011 at 14:11

    • 浮点数存储规则,0~51位是尾数,52~62位是指数,63位是符号位,
      指数有0x3ff的偏移量。
      所以,前面二进制的53个1存储的尾数就是”f ffff ffff ffff”,最高位的1规格化后不用存储。
      符号位加上11位的指数(指数有0x3ff的偏移量)就是”7fe”。

      zeratul1986

      April 2, 2011 at 14:28

      • 我明白了,7fef ffff ffff ffff 是存储位的 16 进制表示,这样理解就很清晰了,不错。

        我的题目原意是纯数学题,不能用 javascript 代码去验证的,因为 x 的值在 js 里无法精确表示,不要被绕进去,呵呵。

        lifesinger

        April 2, 2011 at 14:35

      • 看来我是被绕进去了。哈哈

        zeratul1986

        April 2, 2011 at 14:43

    • @lifesinger 你终于出来答题了,其实还有个笨办法。Number.MAX_VALUE.toString(2).replace(/1/g,”)。length = 971,
      由于2的2进制为10,所以我们去掉一个0,那么长度为970个0。如果不存在精度丢失,比如使用BigDecimal的话则值为(2^970) – 1。

      在IEEE中当E=2047和F=zero时才是Infinity。ECMAScript 规范 里有给出 e 的范围为 [-1074, 971],那么
      Number.MAX_VALUE的E应该是11111111110
      Number.MAX_VALUE的F为11111111111。。。。(52位)。
      谁来解释这运算:转换E的值为11111111111,F的值为00000000。。。(52位)。

      谁来解释一下面的运算

      mashihua

      April 2, 2011 at 16:09

      • 根据 s * f * 2^(e), 对于 Number.MAX_VALUE, e 的最大值就是 971 呀,f 的最大值为 (2^53 – 1)

        马兄说的转化是啥意思?

        lifesinger

        April 2, 2011 at 16:19

      • 哦,忙晕了。zeratul1986已经解释清楚了。

        7fe二进制就是11111111110,f ffff ffff ffff二进制就是11111111111。。。。(52位)。

        在IEEE中当E=2047和F=zero时才是Infinity。Infinity的存储是7ff0 0000 0000 0000.

        转换的意思就是通过存储方式的变换推理出一个为X的值。

        mashihua

        April 2, 2011 at 22:27

  14. console.info(2.3*100);
    你猜结果是多少?

    Vesper

    April 2, 2011 at 17:02

  15. […] 原文链接:JavaScript 中小数和大整数的精度丢失 […]

  16. 你好
    javascript最大值这我有点疑问,能帮忙看下是哪出问题了吗

    ecma中规定指数最大值为971

    那么根据ieee 754标准

    这个最大值在内存中应该是
    0 11111001010 1111111111111111111111111111111111111111111111111111
    符号位 971+1023偏移 尾数

    但是把这个值转换成16进制为7cafffffffffffff 再通过这个网站http://babbage.cs.qc.edu/IEEE-754/64bit.html换算下
    结果为:3.9916806190694390e+292
    想不明白是哪的问题了。。。

    zk

    November 11, 2011 at 14:08

  17. 我就碰到了这问题,然后经过测试,发现js不管支持多大的数字,就整数而言,能保留精度的只能有16位,第17位开始就取整补0了

    walkerwzyer

    July 20, 2012 at 16:51

    • m 是尾数,有 52 bits 那么应该是2的52次方减去1。

      non

      February 25, 2014 at 09:35


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s