今天遇到一个问题,前端请求后台保存了一个小数1.2,然后前端后续读取的时候,返回了1.200000000(n个0)...43(一些数字)。
哪呢,难道是跟0.1 + 0.2 !== 0.3一个道理?
经过我的实验,发现跟0.1 + 0.2 !== 0.3不是同一个问题。实验最后的结论是:
- 位数<=16位的小数的读写,无需考虑精度丢失问题
- 违反直觉的是,代码写的小数,与打印来的小数不一定相同。JavaScript/Python等语言会在第17小数四舍五入。
下面是我的分析过程:
问题提出:既然不能精确地存储小数,又为什么能够正确地打印?
后台同学表示,查询数据库显示的确实是1.2,但是实际上你知道的,小数无法在计算机中被精确存储,所以你将后台返回的小数round一下就好。
我觉得这样不妥,因为1.2是用户手工输入的数字,后台居然没有办法保证原样返回?而且连1位小数都无法精确存储。另外只是简单读写,又不是0.1 + 0.2之类的运算。自己之前也做过java full stack,之前怎么就没遇到过这种奇怪的问题呢。难道需要数据库必须要使用类似MySQL Decimal的类型吗?
我们写代码 a = 0.1, 是可以预期打印出来是0.1的,同样的道理,我把一个1.2小数存储进去数据库,虽然存进去的可能是1.1999999999999999555910790149937383830547332763671875,但是当数据库再次将小数读出来,然后序列化成可读字符之后,应该还是1.2。
这里有两个问题需要研究。
- 计算机虽然无法精确存储0.1,但是为什么能正确打印出0.1,而不是0.1000000000000000055511151231257827021181583404541015625?
- 有没有可能存在一个小数,代码写的是m, 打印出来却是n?
实验验证
这里,我通过chrome console是研究这两个问题。 首先,javascript有个函数是toPrecision,可以查看小数的精度。 例如
然后,我尝试找出一个小数,打印结果与代码不一样。结果,还真的找到了,但是小数数位比较多的情况才能满足这个情况。例如:
这里就有一个结论,计算机除了不能精确地存储小数,还是不一定能按照代码正确地打印小数。我们平时写代码console.log(0.1),预期能打印0.1,人觉得正常,但是从计算机角度,反而可能是一种特例。
我接着研究,为什么打印结果比代码输入的数字小
对齐下数字,很明显,就是四舍五入的结果
拿更多的数字来研究,我发现一个大致的规律:
1. 如果第17位数字 <=4,那么会舍弃,0.1, 0.2,0.10000000000000001 就是这种情况。
2. 如果第17位数字 >=5, 那么会保留,并且会从18位数字进位。
这里我想到一个问题,是不是小数数字位数 >= 17,才可能会代码输入的小数和打印的结果不一致的情况呢?
这里我利用穷举的方式去证明。 注意,不能使用+0.000000000...0000001的方式去循环穷举,因为精度问题会跳过一些数字,所以我使用了字符串方式去穷举。
另外由于javascript可能会使用科学记数法去打印小数,所以我使用0.1前缀作为穷举小数的前缀。
Run以下代码,
// find all value that's 'not equal to itself'.
// 0.10000000000000008.toString() === '0.10000000000000007' is true!
const n = 17;
const numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
function solution() {
const result = [];
backtrack([]);
console.log(result)
function backtrack(track) {
// The prefix part of the number is always 0.1, so we only traverse n-1
if (track.length === n-1) {
const track2 = track.slice(0);
while(track2[track2.length - 1] === '0'){
track2.splice(track2.length - 1, 1);
}
track2.unshift('0.1')
const code = `if ("${track2.join('')}" !== (${track2.join('')}).toString()) {
console.log("${track2.join('')}", "!==", (${track2.join('')}).toString());
}`;
// console.log(code);
eval(code);
return;
}
for (let i=0; i<numbers.length; i++) {
track.push(numbers[i]);
backtrack(track);
track.pop();
}
}
}
solution();
17位小数,代码输入的小数和打印结果不一致的,非常非常多。
但是我们将小数位数 n 改成16,发现就没有输出结果了。
所以只有小数数字位数 >= 17,才可能会出现,代码输入的小数和打印的结果不一致的情况
其实,在其他语言,应该也是类似的实现,例如我把
0.10000000000000008
拷贝到python REPL,也是跟javascript一样的结果
所以,我有理由相信,是后台处理出现的问题,因为用户输入的数字仅仅是1位小数而已,远远没有达到17位小数。
最后,后台发现是C++代码出现了问题。后台使用C++ double类型写入数据库,但是却使用float类型读取数据库。
结论
- 位数<=16位的小数的读写,无需考虑精度丢失问题
- 违反直觉的是,代码写的小数,与打印来的结果不一定相同。JavaScript/Python等语言会在第17小数四舍五入。