浮点数学习心得
问题起因
因为前几天遇到一个问题,量表配了一个小数0.099,通过转表工具转成json后变成了0.1,对此很是困惑不解,并进行一步步研究
-
1、先检查了量表却是配的是0.099,而且0.098转出是正常的
-
2、我用python写的工具转了一遍却能正常转出0.099,难道是我们的转表工具有问题?
-
3、我们的转表工具是VS工程的C#语言写的,对此我又搭了一个临时windows环境去装VS,但临时版的windows有很多问题,不能安装,我就直接去看了一下C#代码,看了许久发了了这样一段代码
string temp = value.ToString(); Regex r; r = new Regex("[0-9]*\\.(.+?)9*99$"); Match m = r.Match(temp); if (m.Success) { string lengthStr = m.Groups[1].Value; int length = lengthStr.Length + 1; value = Math.Round(value, length); }
研究结果
虽然C#用得少,但猜测能够看得出是检查数字匹配到99结尾的小数时,会对value进行了重新赋值,转成了0.1,那之前的大神为什么要做这个操作了?因此对小数做了一些研究。在计算机里,数字分为整数和小数,我们对于整数的认识、运用相信大家都炉火纯青了,但对于小数可能还是有些疏漏的,比说我们经常会看到的现象
- 1、策划量表配了1.139,转到我们的json文件里却是1.13899999999998
- 2、为什么(0.1+0.2)==0.3,这个判断是false
为了解决这个问题,首先我们看一下小数是怎么保存的
浮点数存储方式
电脑所有数据是二进制储存的,1位就是电脑一个最小单位空间,只能储存0或1的值,对于整数的二进制转换大家都很熟了,这里我就不多说了,那么小数呢?
小数即浮点数就是带小数点的值,在电脑储存的类型一般有:
- float 32位 学名单精度浮点数
- double 64位类型 学名双精度浮点数;
- long double 80~128位(各语言储存位数可能不一样反正都是大于64的 %8==0)
意味着他们这些类型用32 64 80~128个最小空间表示一个浮点数,这些类型储存分为3段存储的.
- 第一段 占用空间1位保存浮点数的正负值 学名数符
- 第二段 占用空间8 or 11 or >=15位 学名阶码 决定浮点数的取值范围
- 第三段 占用空间23 or 52 or >=64位 学名尾数 决定浮点数的精度
浮点数类型都是由下面这个模式储存的:
数符 | 阶码 | 尾数 |
---|---|---|
0 | 10000001 | 11111111111111111111111 |
数符
- 1位
- 值为0 代表float类型的数为正数,
- 值为1 代表float类型的数为负数
阶码
- 8位
- 值为N最小为0 最大为 255 表示后面尾数中小数点的位置,也即是浮点的指数
- 阶码为0,尾数全0的时候表示0由于
- 阶码为1,尾数全0的时候表示无穷大
- 在32位浮点数表示中0和255要去掉,即阶码取值范围变为1~254
- 由于指数有正数和负数之分,为了让指数指数取值范围包含正数负数,对N做了一个偏移操作,偏移量选取中间值127,(N-127),这样浮点数指数的取值范围就变成了 -126 ~ +127,因此浮点数的取值范围是2(-126)~2127
- 在二进制中 乘2代表 所有被乘的数左移一位末尾添加0,例二进制:111*2=1110,除2: 111/2 = 0111 (原1的位置变成0)
- 那么2^N中N就代表了这串二进制左移次数N或理解为右侧插入N个0,-N代表右移N次或理解为左侧插入N个0;
尾数
- 某种算法保存浮点数值的二进制数;
- 储存时它没有储存最前面位数的1,这里的浮点数是科学计数法的形式保存的
- 什么是浮点数科学记数法,比如0.001101可以写成0.001101*20,用科学记数法也可以写成1.101*2(-3)
- 尾数不保存最前面的1,那当前数的尾数为1010000 00000000 00000000
二进制浮点数
举个例子,二进制浮点数 0 10000001 1101000 00000000 00000000
- 数符 0 正数
- 阶码 10000001 为129,即指数为 e = N-127 = 129-127 = 2
- 尾数 1101000 00000000 00000000,还原科学记数法左边的1为11101000 00000000 00000000
- 尾数对阶码指数2做乘法运算,即尾数左移两位,右边补00,即为11 10100000 00000000 00000000
- 尾数是23位,将尾数开头位分开为 111 0100000 00000000 00000000
- 那么111即为整数的部分 1*2^2 + 1*2^1 + 1*2^0 = 7
- 0100000 00000000 00000000为小数部分 0*2^(-1) + 1*2^(-2) + 0 = 0.25
- 所以合在一起为7.25
转表问题解决
知道浮点数在计算机里的保存方法以后,我们在回头解释前面提到的疑问
为什么我们写的转表工具会对结尾为99的浮点数做进位处理,为什么1.139,转到我们的json文件里却是1.13899999999998
这两个是一个原理

比如说1.139转成二进制的过程是(可以在网上在线转换)
整数部分1,二进制为1
小数部分0.139,需要用*2方法进行转换
- 0.139 *2 0.278 0
- 0.278 *2 0.556 0
- 0.556 *2 1.112 1
- 0.112 *2 0.224 0
- 0.224 *2 0.448 0
- 0.448 *2 0.896 0
- 0.896 *2 1.792 1
- 0.792 *2 1.584 1
- 0.584 *2 1.168 1
- 0.168 *2 0.336 0
- …过程太长了省略掉了
由于网上只能转到52位,所以我用代码转了一下,随便取到54位
local strs = "0."
local nums = 0.139
for i = 1, 54 do
local newNum = nums * 2
if newNum > 1 then
nums = newNum - 1
strs = strs .. "1"
else
nums = newNum
strs = strs .. "0"
end
end
- 结果为 001000111001010110000001000001100010010011011101001011(54位)
- 而在64位浮点数里只能保留52位的精度即取了前面的52位
- 即为00100011 10010101 10000001 00000110 00100100 11011101 0010
- 那么后两位11以及后面的精度将会丢失,我们用前52位转回十进制看是1.1389999999999998
- 确实丢失52以后的精度以后会少那么一点点,这就是为什么我们转表后不是1.139的原因
- 转表工具对99结尾的浮点做处理就是为了弥补这一点精度丢失,但其实不是很准确,或许需要多匹配几位数会更好,不过这里不纠结了
0.1+0.2~=0.3问题解决
那么我们看另外一个疑问0.1+0.2~=0.3的原因
- 0.1的二进制是0.0001100110011001100110011001100110011001100110011001100110011…
- 0.2的二进制是0.0011001100110011001100110011001100110011001100110011001100110…
- 0.1+0.2做运算后会保存为中间值只取了52位,即
- 0.1+0.2 = 0.01001100 11001100 11001100 11001100 11001100 11001100 1100
- 转回十进制是0.2999999999999998 ~= 0.3
- 所以0.3 ~= 0.1+0.2是正常的
问题应对
我们一般不对浮点数进行相等比较,但如果需要的时候我们如何应对这种精度误差呢
- 方法1,转字符串在比较
- print(0.1+0.2==0.3) 为false
- print(tostring(0.1+0.2)==tostring(0.3)) 为true
- 方法2,做一个差值与一个设定精度比较
- 0.1+0.2-0.3 < 1e-5 也是可以的
扩展
对于整数大数据的科学计数法精度问题也是同一个原理
参考链接:浮点数在计算机二进制储存的问题