【Unity3d】C#浮点数丢失精度问题

news2025/5/15 10:31:50

一、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.coloradmin.cn/o/2270066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Browser Use:AI智能体自动化操作浏览器的开源工具

Browser Use:AI智能体自动化操作浏览器的开源工具 Browser Use 简介1. 安装所需依赖2. 生成openai密钥3. 编写代码4. 运行代码5. 部署与优化5.1 部署AI代理5.2 优化与扩展总结Browser Use 简介 browser-use是一个Python库,它能够帮助我们将AI代理与浏览器自动化操作结合起来;…

tcpdump指南(1)

大家读完觉得有意义记得关注和点赞&#xff01;&#xff01;&#xff01; tcpdump是一种在网络上转储流量的网络工具。 这篇文章服务器作为一些常用命令的指南。如需完整指南&#xff0c; 请参阅手册页&#xff0c;或在 Linux 计算机上。man tcpdump 1 基本选项 帮助摘要&#…

14. 日常算法

1. 面试题 02.04. 分割链表 题目来源 给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你不需要 保留 每个分区中各节点的初始相对位置。 class Solution { public:ListNo…

termux-boot安卓开机自动启动应用

termux安装 github 蓝奏云 v119.1 termux-boot安装 github 蓝奏云 v0.8.1 安装 给权限运行加锁后台 am启动应用命令 am start -n 包名/启动项获取包名和启动入口&#xff08;图中app为爱玩机工具箱&#xff09; 例 简黑时钟蓝奏云 包名com.hm.jhclock 桌面启动项com.hm.jh…

自从学会Git,感觉打开了一扇新大门

“同事让我用 Git 提交代码&#xff0c;我居然直接把项目文件压缩发过去了……”相信很多初学者都经历过类似的窘境。而当你真正掌握 Git 时&#xff0c;才会发现它就像一本魔法书&#xff0c;轻松解决代码管理的种种难题。 为什么 Git 能成为程序员的标配工具&#xff1f;它究…

设计模式 创建型 建造者模式(Builder Pattern)与 常见技术框架应用 解析

单例模式&#xff08;Singleton Pattern&#xff09;&#xff0c;又称生成器模式&#xff0c;是一种对象构建模式。它主要用于构建复杂对象&#xff0c;通过将复杂对象的构建过程与其表示分离&#xff0c;使得同样的构建过程可以创建出具有不同表示的对象。该模式的核心思想是将…

MATLAB程序转C# WPF,dll集成,混合编程

工作中遇到一个需求&#xff0c;有一部分算法的代码需要MATLAB来进行处理&#xff0c;而最后需要集成到C#中的wpf项目中去&#xff0c;选择灵活性更高的dll&#xff0c;去进行集成。&#xff08;可以简单理解为&#xff1a;将MATLAB的函数&#xff0c;变为C#中类的函数成员&…

常见中间件漏洞复现

1.tomcat 1.1 CVE-2017-12615(put上传) 当在Tomcat的conf&#xff08;配置目录下&#xff09;/web.xml配置文件中添加readonly设置为false时&#xff0c;将导致该漏洞产 ⽣&#xff0c;&#xff08;需要允许put请求&#xff09; , 攻击者可以利⽤PUT方法通过精心构造的数据包…

C#Halcon深度学习预热与否的运行时间测试

在深度学习推理应用阶段&#xff0c;涉及到提速&#xff0c;绕不开一个关键词“预热”。 在其他地方的“预热”&#xff0c;预先加热到指定的温度。通常指预习准备做某一样事时&#xff0c;为此做好准备。 而在深度学习推理应用阶段涉及的预热通常是指GPU预热&#xff0c;GPU在…

获取 Astro Bot AI 语音来增强您的游戏体验!

有很多用户尝试过Astro Bot&#xff0c;却被Astro Bot可爱的声音所吸引。您是否想知道如何使用 Astro Bot 语音来拨打恶作剧电话或用他的声音说话&#xff1f;如果您有&#xff0c;那么这篇文章适合您。我们将向您展示如何为 Astro Bot 提供逼真的 AI 声音并在在线对话中使用它…

重装操作系统后 Oracle 11g 数据库数据还原

场景描述&#xff1a; 由于SSD系统盘损坏&#xff0c;更换硬盘后重装了操作系统&#xff0c;Oracle数据库之前安装在D盘(另一个硬盘)&#xff0c;更换硬盘多添加一个盘符重装系统后盘符从D变成E&#xff0c;也就是之前的D:/app/... 变成了现在的 E:/app/...&#xff0c;重新安装…

IDEA+Docker一键部署项目SpringBoot项目

文章目录 1. 部署项目的传统方式2. 前置工作3. SSH配置4. 连接Docker守护进程5. 创建简单的SpringBoot应用程序6. 编写Dockerfile文件7. 配置远程部署 7.1 创建配置7.2 绑定端口7.3 添加执行前要运行的任务 8. 部署项目9. 开放防火墙的 11020 端口10. 访问项目11. 可能遇到的问…

UE5材质节点Camera Vector/Reflection Vector

Camera Vector相机向量&#xff0c;输出像素到相机的方向&#xff0c;结果归一化 会随着相机移动而改变 Reflection Vector 反射向量&#xff0c;物体表面法线反射到相机的方向&#xff0c;x和y和camera vector相反 配合hdr使用

python实现自动登录12306抢票 -- selenium

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 python实现自动登录12306抢票 -- selenium 前言其实网上也出现了很多12306的代码&#xff0c;但是都不是最新的&#xff0c;我也是从网上找别人的帖子&#xff0c;看B站视频&…

鸿蒙1.2:第一个应用

1、create Project&#xff0c;选择Empty Activity 2、配置项目 project name 为项目名称&#xff0c;建议使用驼峰型命名 Bundle name 为项目包名 Save location 为保存位置 Module name 为模块名称&#xff0c;即运行时需要选择的模块名称&#xff0c;见下图 查看模块名称&…

Docker安装(Docker Engine安装)

一、Docker Engine和Desktop区别 Docker Engine 核心组件&#xff1a;Docker Engine是Docker的核心运行时引擎&#xff0c;负责构建、运行和管理容器。它包括守护进程&#xff08;dockerd&#xff09;、API和命令行工具客户端&#xff08;docker&#xff09;。适用环境&#…

Ceph 手动部署(CentOS9)

#Ceph手动部署、CentOS9、squid版本、数字版本19.2.0 #部署服务:块、对象、文件 一、部署前规划 1、兼容性确认 2、资源规划 节点类型节点名称操作系统CPU/内存硬盘网络组件安装集群节点CephAdm01CentOS94U/8GOS:40G,OSD:2*100GIP1:192.169.0.9(管理&集群),IP2:…

springboot-启动流程

by shihang.mai 1. 启动流程图 查看springboot2.3.7源码后绘制下图。 springboot启动流程图 main启动&#xff0c;new SpringApplication()构造方法里面 判断当前应用程序类型reactive、servlet、none获取所有的ApplicationListener对应的对象获取所有的ApplicationContextInit…

Flutter:打包apk,详细图文介绍(一)

困扰了一天&#xff0c;终于能正常打包apk安装了&#xff0c;记录下打包的流程。建议参考我这篇文章时&#xff0c;同时看下官网的构建说明。 官网构建并发布 Android 应用详情 1、AS创建Flutter项目 2、cmd执行命令 生成一个sunluyi.jks的文件&#xff0c;可以自行把sunluyi替…

linux下安装tun模块详细教程

原本是要看tcp/ip协议栈&#xff0c;找到了https://github.com/chobits/tapip这个。然后需要支持tun&#xff0c;完了开始安装。 TUN/TAP是一个虚拟网络设备&#xff0c;用于实现用户态程序和内核网络协议栈之间的数据交互。 1、安装环境 我的系统是ubuntu16.04 VMWare虚拟机&a…