浮点数学习心得

浮点数学习心得

问题起因

因为前几天遇到一个问题,量表配了一个小数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
这两个是一个原理

扫描二维码关注公众号,回复: 12442961 查看本文章

比如说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 也是可以的

扩展

对于整数大数据的科学计数法精度问题也是同一个原理

参考链接:浮点数在计算机二进制储存的问题

猜你喜欢

转载自blog.csdn.net/weixin_41722502/article/details/107767804