当前位置: 首页 > article >正文

【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   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

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)


http://www.kler.cn/a/463492.html

相关文章:

  • df.groupby()方法使用表达式分组
  • 服务器数据恢复—服务器硬盘亮黄灯的数据恢复案例
  • ubuntu切换到root用户
  • Ubuntu 安装英伟达显卡驱动问题记录
  • docker 安装influxdb
  • CDP集群安全指南-静态数据加密
  • 如何查询快手IP归属地?如何关闭
  • HTML——46.制作课程表
  • 鸿蒙应用开发 - 如何去掉字符串中空格
  • 使用 `Celery` 与 `RabbitMQ` 实现异步任务队列:构建高效、可靠的任务调度系统
  • 深度学习在光学成像中是如何发挥作用的?
  • [创业之路-222]:波士顿矩阵与GE矩阵在业务组合选中作用、优缺点比较
  • 如何通过深度学习提升大分辨率图像预测准确率?
  • Ajax阶段总结(二维表+思维导图+四种请求方式)
  • 数据库概念(MySQL第一期)
  • MongoDB 固定集合
  • AWTK 在 ESP 上的移植笔记
  • quasar v2 setup语法中报错: undefined is not an object (evaluating ‘this.$q.notify‘)
  • 使用 Actix-Web、SQLx 和 Redis 构建高性能 Rust Web 服务
  • 电子电气架构 --- 安全相关内容汇总
  • HeidiSQL导入与导出数据
  • 【GO基础学习】Go操作数据库MySQL
  • webpack打包node后端项目
  • 3维场景测试自动化
  • 18.2、网络安全评测技术与攻击
  • excel怎么删除右边无限列(亲测有效)