-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript 深入之浮点数精度 #155
Comments
大神好久没更新了 先占楼慢慢看~ |
看到更新了,很惊喜~ |
大佬终于更新了 深入系列又刷一遍 写的真是太好了 |
哇,大佬又开始更新了,沙发!!! |
1.tcp三次握手的原理和过程,tcp与udp有什么区别 |
这两天刚刚遇到了这个问题,这波跟新来的太及时了 |
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog。 👈这里多了个句号 点进去是 404 |
突然晕了,既然0.1在存储的时候就已经精度丢失了,那么1 - 0.1 == 0.9是怎么计算的 |
用 52 位存储 Fraction,那Fraction最大能存储多大的数字呢? |
可能是0.1精度丢失舍入处理向上进位了,0.2也是向上进位的,所以加起来比0.3大 |
64位跟64位字节好像有歧义 冴羽回复:感谢你的指正,犯了很低级的错误,64位是 8 字节,目前已经更正了 |
这里没有理解,为什么是0~254呢,2^8的256,去掉一个0,是255. 2位的话,2^2 = 4, 二进制分别是,00,01,10,11,表达范围是,0,1,2,3 也就是 闭区间[0,3] 这里是遗漏了什么细节么?麻烦各位大佬解答下。 我这算了下,不知道公式对不对。 |
确实,位是bit,字节是byte,1byte = 8bit |
强啊,思路很清晰,正好前几天也在研究
|
1 表示成科学计数法: 1 * 2^0 * 1
即结果是 0.11100110011001100110011... |
我也有点晕,既然0.1在存储的时候就已经精度丢失了,那为么为什么字面量的0.1 在控制台打印出来还是0.1 |
我的理解是 2进制的八位字节 也就是 |
八位字节 无符号的情况下 不应该是0~255 吗 请大佬解答一下 |
提一下十一位的指数位,虽然十一位表示的范围是 0 到 2047,实际上 0 和 2047 会被作为特殊的数解析(0,NaN,Inifinity),因此实际用来表示正常数的范围是 1-2046,算上基数,对应的就是 -1022 ~ 1023,所以 Number.MAX_VALUE 的计算方法是 1.11111(二进制数,小数点后 52 个1) * (2 ** 1023),最后是 1023 而不是 1024 次方。参考链接 |
有规定的,浮点数E全1和全0有特殊意义,不能取全1,不久最大只能是11111110吗? |
@ominus3 非常感谢你的回复和补充 |
以下解释来自百度百科“规格化浮点数”
|
前言
0.1 + 0.2 是否等于 0.3 作为一道经典的面试题,已经广外熟知,说起原因,大家能回答出这是浮点数精度问题导致,也能辩证的看待这并非是 ECMAScript 这门语言的问题,今天就是具体看一下背后的原因。
数字类型
ECMAScript 中的 Number 类型使用 IEEE754 标准来表示整数和浮点数值。所谓 IEEE754 标准,全称 IEEE 二进制浮点数算术标准,这个标准定义了表示浮点数的格式等内容。
在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度,也就是说,会用 64 位来储存一个浮点数。
浮点数转二进制
我们来看下 1020 用十进制的表示:
所以 1020 用十进制表示就是 1020……(哈哈)
如果 1020 用二进制来表示呢?
所以 1020 的二进制为
1111111100
那如果是 0.75 用二进制表示呢?同理应该是:
因为使用的是二进制,这里的 abcd……的值的要么是 0 要么是 1。
那怎么算出 abcd…… 的值呢,我们可以两边不停的乘以 2 算出来,解法如下:
两边同时乘以 2
剩下的:
再同时乘以 2
所以 0.75 用二进制表示就是 0.ab,也就是 0.11
然而不是所有的数都像 0.75 这么好算,我们来算下 0.1:
然后你就会发现,这个计算在不停的循环,所以 0.1 用二进制表示就是 0.00011001100110011……
浮点数的存储
虽然 0.1 转成二进制时是一个无限循环的数,但计算机总要储存吧,我们知道 ECMAScript 使用 64 位来储存一个浮点数,那具体是怎么储存的呢?这就要说回 IEEE754 这个标准了,毕竟是这个标准规定了存储的方式。
这个标准认为,一个浮点数 (Value) 可以这样表示:
看起来很抽象的样子,简单理解就是科学计数法……
比如 -1020,用科学计数法表示就是:
sign 就是 -1,exponent 就是 10^3,fraction 就是 1.02
对于二进制也是一样,以 0.1 的二进制 0.00011001100110011…… 这个数来说:
可以表示为:
其中 sign 就是 1,exponent 就是 2^-4,fraction 就是 1.1001100110011……
而当只做二进制科学计数法的表示时,这个 Value 的表示可以再具体一点变成:
(如果所有的浮点数都可以这样表示,那么我们存储的时候就把这其中会变化的一些值存储起来就好了)
我们来一点点看:
(-1)^S
表示符号位,当 S = 0,V 为正数;当 S = 1,V 为负数。再看
(1 + Fraction)
,这是因为所有的浮点数都可以表示为 1.xxxx * 2^xxx 的形式,前面的一定是 1.xxx,那干脆我们就不存储这个 1 了,直接存后面的 xxxxx 好了,这也就是 Fraction 的部分。最后再看
2^E
如果是 1020.75,对应二进制数就是 1111111100.11,对应二进制科学计数法就是 1 * 1.11111110011 * 2^9,E 的值就是 9,而如果是 0.1 ,对应二进制是 1 * 1.1001100110011…… * 2^-4, E 的值就是 -4,也就是说,E 既可能是负数,又可能是正数,那问题就来了,那我们该怎么储存这个 E 呢?
我们这样解决,假如我们用 8 位来存储 E 这个数,如果只有正数的话,储存的值的范围是 0 ~ 254,而如果要储存正负数的话,值的范围就是 -127~127,我们在存储的时候,把要存储的数字加上 127,这样当我们存 -127 的时候,我们存 0,当存 127 的时候,存 254,这样就解决了存负数的问题。对应的,当取值的时候,我们再减去 127。
所以呢,真到实际存储的时候,我们并不会直接存储 E,而是会存储 E + bias,当用 8 位的时候,这个 bias 就是 127。
所以,如果要存储一个浮点数,我们存 S 和 Fraction 和 E + bias 这三个值就好了,那具体要分配多少个位来存储这些数呢?IEEE754 给出了标准:
在这个标准下:
我们会用 1 位存储 S,0 表示正数,1 表示负数。
用 11 位存储 E + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。
用 52 位存储 Fraction。
举个例子,就拿 0.1 来看,对应二进制是 1 * 1.1001100110011…… * 2^-4, Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,Fraction 是 1001100110011……
对应 64 位的完整表示就是:
同理, 0.2 表示的完整表示是:
所以当 0.1 存下来的时候,就已经发生了精度丢失,当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数。
浮点数的运算
关于浮点数的运算,一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断。我们来简单看一下 0.1 和 0.2 的计算。
首先是对阶,所谓对阶,就是把阶码调整为相同,比如 0.1 是
1.1001100110011…… * 2^-4
,阶码是 -4,而 0.2 就是1.10011001100110...* 2^-3
,阶码是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,也就是 0.1 的 -4 调整为 -3,对应变成0.11001100110011…… * 2^-3
接下来是尾数计算:
我们得到结果为
10.0110011001100110011001100110011001100110011001100111 * 2^-3
将这个结果处理一下,即结果规格化,变成
1.0011001100110011001100110011001100110011001100110011(1) * 2^-2
括号里的 1 意思是说计算后这个 1 超出了范围,所以要被舍弃了。
再然后是舍入,四舍五入对应到二进制中,就是 0 舍 1 入,因为我们要把括号里的 1 丢了,所以这里会进一,结果变成
1.0011001100110011001100110011001100110011001100110100 * 2^-2
本来还有一个溢出判断,因为这里不涉及,就不讲了。
所以最终的结果存成 64 位就是
将它转换为10进制数就得到
0.30000000000000004440892098500626
因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3
其他
参考
深入系列
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: