cjmm / cjmm.github.io

周木的个人简历 🍻
https://cjmm.github.io
MIT License
1 stars 0 forks source link

IEEE 754 ( javascript ) 如何精确浮点运算 #25

Open superwoods opened 8 years ago

superwoods commented 8 years ago

为什么不精确:

js中浮点运算,例如:

0.1 + 0.2
// 0.30000000000000004

无法得到预期结果 0.3

不仅在 JavaScript 中存在这个「问题」,所有的支持二进制浮点数运算(绝大部分都是 IEEE 754 的实现)的系统都存在这个现象。

其原因就是,在有限的存储空间下,绝大部分的十进制小数都不能用二进制浮点数来精确表示。例如,0.1 这个简单的十进制小数就不能用二进制浮点数来表示。(@彭晟杰)

要想清楚这个问题,就得搞明白浮点数的设计、实现上因为“硬件的先天性缺陷”导致的精确度问题了。 我觉得可以总结为一句话:用离散的数据模型去模拟实际上无穷的实数集合,误差是永远无法避免的。(@聂伟琳)

以上摘选自知乎两位作者的文章,他们答案中说的很详细,原文地址:https://www.zhihu.com/question/20679634

解决思路和吐槽一个朋友:

在有限的存储空间下,绝大部分的十进制小数都不能用二进制浮点数来精确表示。

那么js中解决这个问题的关键就是:

不用浮点数相加,让他们放大为整数相加,再缩小变为浮点数

所以简而言之,解决的关键在于小数点上,虽然前几天有个朋友跟说不是这样的,重点是在字符串上,但是我仍然觉得我并没有说错,问题的关键还是去掉小数点嘛!

于是,我们可以这么做:

  1. 0.1 放大10倍变为 10.2 放大10倍变为 2
  2. 1 + 2 = 3
  3. 再把 3 缩小 10 倍,得道正确结果 0.3;

如果是这么想来,字符串不是重点吧?只是把数值变为字符串而已,但后面还是用小数点分割数组了。。。

但是我不得不承认,作为非工科出身文科形象思维的我,思维方式中是很难一下子想到这样的抽象思维或者说数学解决方案的,这个思考方式的转换,似乎是一个哲学问题,一个事为什么会这么想,很多时候不是我们决定的,可能会慢慢转变,所以结果就是我还是被你打败了,下次见面不如咱们聊聊怎么画素描吧? 😄开个玩笑~~

不过还是要感谢你,朋友!因为你这个小问题,所以我弄清楚了为什么浮点运算会这样子,短期看来对工作似乎没大帮助,但对于编程的思维方式培养和使用数学方法解决编程中的问题的习惯是有帮助的,这依然是我欠缺的东西,祝福你,也祝福我自己,只有知道自己的不足才能让自己进步不是吗?

再次感谢你!加油!

最后是js实现:

function add(v1, v2) {
  var r1, r2, m;
  try {
    r1 = v1.toString().split(".")[1].length; 
  } catch (e) {
    r1 = 0;
  }
  console.log('r1:', r1); // 获取v1剪切小数点的后的位数 "r1:" 1
  try {
    r2 = v2.toString().split(".")[1].length;
  } catch (e) {
    r2 = 0;
  }
  console.log('r2:', r2); // 获取v2剪切小数点的后的位数 "r2:" 1
  m = Math.pow(10, Math.max(r1, r2)); // 使用 Math.pow 获取倍数,10的r1、r2中取最大值次幂。
  console.log('m:', m); // "m:" 10
  return (v1 * m + v2 * m) / m;
}
var a = 0.1;
var b = 0.2;
console.log(add(a, b)); // 0.3
console.log(a + b); // 0.30000000000000004

codepen地址:

See the Pen js精确加法 by Super Woods (@superwoods) on CodePen.

superwoods commented 8 years ago

2016-09-22: 近来,夜观星象。。。 呃 夜观代码,有所心得故而键录于此,聊以自慰!

js 乘法亦有浮点运算之问题:

加法、乘法,哎! 正应了三国曹植七步成诗:“本是同根生,相煎何太急”, 但木木说此处应改为:

”都是754,加乘别积极“ ,老实儿 / 10

// 不符合预期:
for(let i=0, j=100; i<j; i++) { console.log(i++ * 0.1); }
/*
2016-09-22 09:01:52.113 VM6449:1 0
2016-09-22 09:01:52.113 VM6449:1 0.2
2016-09-22 09:01:52.113 VM6449:1 0.4
2016-09-22 09:01:52.113 VM6449:1 0.6000000000000001
2016-09-22 09:01:52.113 VM6449:1 0.8
2016-09-22 09:01:52.114 VM6449:1 1
2016-09-22 09:01:52.114 VM6449:1 1.2000000000000002
2016-09-22 09:01:52.114 VM6449:1 1.4000000000000001
2016-09-22 09:01:52.114 VM6449:1 1.6
2016-09-22 09:01:52.114 VM6449:1 1.8
2016-09-22 09:01:52.114 VM6449:1 2
...
*/
// 符合预期:
for(let i=0, j=100; i<j; i++) { console.log(i++ / 10); }
/*
2016-09-22 09:04:14.831 VM6452:1 0
2016-09-22 09:04:14.831 VM6452:1 0.2
2016-09-22 09:04:14.831 VM6452:1 0.4
2016-09-22 09:04:14.832 VM6452:1 0.6
2016-09-22 09:04:14.832 VM6452:1 0.8
2016-09-22 09:04:14.832 VM6452:1 1
2016-09-22 09:04:14.832 VM6452:1 1.2
2016-09-22 09:04:14.832 VM6452:1 1.4
2016-09-22 09:04:14.832 VM6452:1 1.6
2016-09-22 09:04:14.832 VM6452:1 1.8
2016-09-22 09:04:14.832 VM6452:1 2
...
*/
superwoods commented 8 years ago

经过 java开发的同事帮助:

Java 中不做 类型转换 也存在这样的问题,看图说话:

ieee 754 java

ps: 感谢我的同事,一位Senior Java程序 哦! 人超好、超可爱的😳 so 刚刚吐槽她的IDE的直接拉出去毙了

mingyun commented 7 years ago

赞一个,另推荐 math.js

superwoods commented 7 years ago

2017-01-24-04.12 补充 “JavaScript高级程序设计” 关于Number类型 浮点数值:

最近读了一下 “JavaScript高级程序设计”, 真是惭愧惭愧,以前都不好好读书的,该死该死啊。。。。

以下引用自原书:

3.4.5 Number 类型

1. 浮点数值

浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远远不如整数。例如,0.1加0.2的结果不是0.3,而是0.30000000000000004。这个小小的舍入误差会导致无法测试特定的浮点数值。例如:

if (a + b == 0.3){
    //不要做这样的测试!
    alert("You got 0.3.");
}

在这个例子中,我们测试的是两个数的和是不是等于0.3。如果这两个数是0.05和0.25,或者是0.15和0.15都不会有问题。而如前所述,如果这两个数是0.1和0.2,那么测试将无法通过。因此,永远不要测试某个特定的浮点数值。

✏️:关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是使用基于IEEE754数值的浮点计算的通病,ECMAScript并非独此一家;其他使用相同数值格式的语言也存在这个问题。

“JavaScript高级程序设计”中还有很多对于IEEE-754的js相关联问题的阐述,感兴趣的话可以自己去看看,就不一一例举了,