【Unity3d】C#浮点数丢失精度问题
一、float、double浮点数丢失精度问题
Unity3D研究院之被坑了的浮点数的精度(一百零三) | 雨松MOMO程序研究院
https://segmentfault.com/a/1190000041768195?sort=newest
浮点数丢失精度问题是由于大部分浮点数在IEEE754规范下就是无法准确以二进制形式存储的,如果无法准确存储,那么自然也无法准确计算数值。
float:4字节 32位 (1位符号位 8位指数位 23小数部分)
double:8字节 64位 (1位符号位 11为指数位 52为小数部分)
decimal的存储计算情况还没了解,而float的情况和double一样,其小数部分是23位存储,但实际有效范围是2^24,double是2^53,因为“IEEE规定,规约化之后的二进制小数,小数点左边必定是1,因此这23位可以表达的最大数值是1.11111111111111111111111(二进制),一共是24位。”
也就是说实际上存储的二进制形式小数,个位的1是约定俗成的(必然是1)所以就节省了这一位的存储了,只存储后面的小数部分(23位)【double类型同理】
下面C#案例解释0.1浮点数为什么无法准确存储【代码使用了Odin插件,可以自行去掉】
准确的IEEE754规范的十进制浮点数转化二进制浮点数公式:
整数部分:除2取余(倒序 高位写入二进制位)
小数部分:乘2取整(顺序 高位写入二进制位)
符号位:0代表正数、1代表负数
指数位:采用增量偏移存储(float32是增量2^7-1,即127) 也就是指数是-4的话会-4+127=123转二进制存储。(double则是增量2^10-1,即1023)
浮点数: 0.1f
符号位0(1)
指数位(8)123 - 127(-4)指数
小数位(23) 总共1 + 8 + 23 = 32位
0 0111 1011 10011001100110011001101
1.10011001100110011001101 * 2 ^ (-4) 注意: 23位是存储小数部分的,IEEE754规则是只存小数部分,实际计算时小数点左边必定是1,2 * (-4)就是小数点位左移4次
0.000110011001100110011001101
上面理解为: 0代表0个0.5 其他则是 0个0.25 0个0.125 1个0.0625 1个0.03125....之和验算小数位是否准确如下, 0.1 取小数部分 * 2取整, 直至没有小数部分为止 取到的整数按顺序作为二进制位(高位到低位)
0.1 * 2 = 0.2 00.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 10.2 * 2 = 0.4 0....注意到进入了一个死循环(0.2、0.4、0.8、0.6 四个数乘2都会有小数,无法乘2得到小数是0的)
0.000110011001100110011001101 这是我们得到的浮点数二进制位显示
仔细发现最后是0011 0011 01,为什么末尾是01 而不是00?猜测是计算机会偏向较大值进位
正常应该是0011 0011 0011 无限循环下去的二进制,由于float32是只有23位因此末尾会被压缩为01
using System;
using Sirenix.OdinInspector;
using UnityEngine;public class MyBigDecimal : MonoBehaviour
{public float num;[HideLabel][LabelText("IEEE754")]public string ieee754;[Button("显示IEEE754二进制字符串形式", ButtonSizes.Large)]public void ShowIEEE754(){float floatValue = num;uint ieee754Value = FloatToIEEE754Converter.ConvertToIEEE754(floatValue);ieee754 = Convert.ToString(ieee754Value, 2).PadLeft(32, '0');//浮点数: 0.1f//符号位0(1)//指数位(8)123 - 127(-4)指数//小数位(23) 总共1 + 8 + 23 = 32位//0 0111 1011 10011001100110011001101//1.10011001100110011001101 * 2 ^ (-4) 注意: 23位是存储小数部分的,IEEE754规则是只存小数部分,实际计算时小数点左边必定是1,2 * (-4)就是小数点位左移4次//0.000110011001100110011001101//上面理解为: 0代表0个0.5 其他则是 0个0.25 0个0.125 1个0.0625 1个0.03125....之和//验算小数位是否准确如下, 0.1 取小数部分 * 2取整, 直至没有小数部分为止 取到的整数按顺序作为二进制位(高位到低位)//0.1 * 2 = 0.2 0//0.2 * 2 = 0.4 0//0.4 * 2 = 0.8 0//0.8 * 2 = 1.6 1//0.6 * 2 = 1.2 1//0.2 * 2 = 0.4 0....注意到进入了一个死循环(0.2、0.4、0.8、0.6 四个数乘2都会有小数,无法乘2得到小数是0的)//0.000110011001100110011001101 这是我们得到的浮点数二进制位显示//仔细发现最后是0011 0011 01,为什么末尾是01 而不是00?猜测是计算机会偏向较大值进位//正常应该是0011 0011 0011 无限循环下去的二进制,由于float32是只有23位因此末尾会被压缩为01}public class FloatToIEEE754Converter{public static uint ConvertToIEEE754(float floatValue){uint num = 0;byte[] bytes = BitConverter.GetBytes(floatValue);if (BitConverter.IsLittleEndian)Array.Reverse(bytes);foreach (byte b in bytes){num <<= 8;num |= b;}return num;}}
}
二、C#、Java解决丢失精度办法
C# 解决办法就是使用decimal,但运算会变慢,decimal为什么能解决精度问题?
decimal类型为什么比float和double精确?
C#中的decimal数据类型是一种用于表示高精度十进制数的数据类型,主要用于金融和其他需要精确计算的场景。
decimal有16字节,128位(bits)
0~15 保留位 16bits
16~23 小数点(左移位数) 8bits
24~30 保留位 7bits
31 符号位(0正数 1负数) 1bits
32~63 高位 32bits
64~95 中位 32bits
96~127 低位 32bits
假设想表达0.6的decimal二进制位形式如下:(与上面对应7行)0000 0000 0000 0000
0000 0001
0000 000
0
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0110计算会将32~127位合并计算出整数是6,符号位31位是0,小数点是1,因此计算得到
(-1^0 * 6) * 10^-1 = 1 * 6 * 0.1 = 0.6 (实际上程序并不会跑我这写的这段代码,因为所有运算+-*/都会用decimal的形式计算,而不是我们理解的运算方法了,如果想把decimal转成小数显示出来,那大概会只是在字符串形式上进行拿到整型后 对小数点字符进行左移而已,猜测是如此)
Java的BigDecimal说明:为什么float和double运算会丢失精度?BigDecimal就一定靠谱?_float转double型会丢精度吗-CSDN博客
C#没有BigDecimal类,github搜索到文章,使用BigInteger实现的。
https://github.com/dparker1/BigDecimal
github: BigDecimal开根号Sqrt方法的算法:竖式开根法——没有计算器也能手算开根
三、为什么十进制浮点数转二进制浮点数的公式是整数除2求余(倒序),小数乘2取整(顺序)
例如:11.25(十进制浮点数)转二进制浮点数
根据二进制位的存储规则小数点前的部分是2^(位阶),小数点前一位的位阶是0,后一位是-1
11.25 拆分为 11 和 0.25 (整数和小数部分)
整数部分解释:
11 = ... + c * 2^2 + b * 2^1 + a * 2^0
11 = 2 * (... + c * 2^1 + b) + a
11/2 = (... + c * 2^1 + b) + a/2
注意:a , b, c ... 这些数必定是0或者1,不会大于2;
很明显将 2*(... + c * 2^1 + b) + a 看出,2*(... + c * 2^1 + b)能被2整除,余数是0;
而a无法被2整除,余数是a;所以,11/2的余数=a值=1,并且11/2的商值=(... + c * 2^1 + b)=5;
以此类推 5 = ... + c * 2^1 + b , 5 = 2 * (...+c) + b , 5/2 = (... + c) + b/2
因此我们通过对整数部分除以2取余数,取完余数继续对整数部分除以2求余,直至整数为0 或 无法填充23个有效位时;从低位填二进制(低位写入 简称倒序)
11 / 2 = 5 (1)
5 / 2 = 2 (1)
2 / 2 = 1 (0)
1 / 2 = 0 (1)
整数部分为0结束
整数部分倒序写入: 1011
小数部分解释:
0.25 = a * 2^-1 + b * 2^-2 + c * 2^-3 + ...
0.25 * 2 = (a * 2^-1 + b * 2^-2 + c * 2^-3 + ...) * 2
= a + (b * 2^-1 + c * 2^-2 + ...)
同理a必定是[0,1],(b * 2^-1 + c * 2^-2 + ...)必定小于1(可以无限接近1),也就是说
a是整数,(b * 2^-1 + c * 2^-2 + ...)是小数
因此我们通过对小数部分乘以2取整,取完整继续取小数部分乘以2 直至为小数为0 或 无法填充23个有效位时(高位写入 简称顺序)
0.25 * 2 = 0.5 (0)
0.5 *2 = 1.0 (1)
小数部分为0,结束。
小数部分顺序写入:01
11.25(十进制)= 1011.01(二进制)
转IEEE754规范:1011.01 = 1.01101 * 2^3
正数符号位写入0、 指数=3+127=130(127是增量偏移)、小数位 01101 (不足23位右补0)
0 | 1000 0010 | 01101 00000 00000 00000 000
符号位(1) | 指数位(8) | 小数位(23)