《帧同步定点数》定点数原理和无损精度的实现方式

本文介绍了定点数的概念及其在帧同步游戏中的作用,重点讲解了位移运算作为实现定点数的高效方法,讨论了位移运算与乘法运算的差异,并提供了C#中隐式和显式转换的原理。文章强调了在转换和运算过程中需要注意的数据精度问题,以确保无误差计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

视屏教程地址:https://www.yxtown.com/user/192182

定点数的由来

定点数是指计算机中采用的一种数的表示方法。参与运算的数的小数点位置固定不变。
一般用于对精度要求比较高的逻辑/数据计算中。
常用于帧同步游戏当中,用来同步和计算逻辑数据。

为什么不使用浮点数 float?

主要是浮点数在不同的平台、硬件、软件、操作系统、汇编指令等不同的环境下,无法保证小数精度的一致性,会产生浮点数误差。这些
误差会导致后续的计算产生蝴蝶效应的偏差,产生错误的结果。

浮点数产生精度误差的原因:

1. 浮点数精度

IEEE 754 标准

IEEE 754 标准:大多数现代计算机系统使用 IEEE 754 标准来表示浮点数。然而,不同的硬件和编译器可能对浮点数的处理方式略有不同,尤其是在舍入和精度方面。

IEEE 754 简单介绍: IEEE二进制浮点数算术标准
是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal
number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。

单精度 vs 双精度

单精度 vs 双精度:单精度浮点数(float)通常有 7 位十进制精度,而双精度浮点数(double)有 15 位十进制精度。在某些平台上,double 类型可能不可用或不被支持。

2. 编辑器问题

编译器优化
不同的编译器可能会对浮点数运算进行不同的优化,这可能导致结果在不同平台上略有差异。例如,某些编译器可能会重新排序浮点数运算以提高性能,但这种重排可能会引入微小的误差。

3.硬件差异

不同的 CPU 和 GPU 在处理浮点数时可能会有不同的行为。例如,某些 GPU 可能会对浮点数进行特定的优化,而这些优化可能与 CPU 的行为不同而产生不同。

不同操作系统可能使用不同的数学库来实现标准的数学函数(如 sin、cos、sqrt 等)。例如,Windows 可能使用 Microsoft 的数学库,而 Linux 和 macOS 可能使用 GNU C 库 (glibc) 或其他库。这些库的实现细节可能不同,导致浮点数运算的结果略有差异。

不同的数学库可能有不同的默认精度和舍入模式。例如,某些库可能使用 IEEE 754 标准的舍入到最接近的偶数模式,而其他库可能使用不同的舍入模式。 而Unity中的强转是默认向下取整,如果浮点数的小数精度不一致,在进行四舍五入或强转时就会产生数据差异问题。

等等等…

什么是定点数

简单来说就是小数点固定的一种值类型数据结构体,可以固定小数点位1位,也可以固定小数点为0为,总之是需要通过一系技术手段来控制小数点的位数,来防止由于小数精度问题带来的数据计算的误差产生的蝴蝶效应。相比于浮点数,定点数具有固定的精度和计算速度较快的优点。

请添加图片描述

但是在真正使用定点数的情况下,没有人愿意保留小数去做运算的哪怕是一位。基本都是保留0位小数,整个逻辑计算完全使用整形数据进行运算,这样才能将误差将为最小。

定点数的作用

1.定点数的作用就是为了解决由于浮点数的浮点特性导致的在不同的平台产生不同步问题。

2.安全、可靠。可以完全放心的把数据交给它。因为它能做到在不同的多个平台计算结果/同步结果全都一致。

3.能够简单有效、无误差的进行战斗逻辑或者数据运算的模拟。因为是定点数的机制,故在数据源相同的情况下,结果永远都是相同的。

4.定点数并非只能作用与帧同步中,也可作用于状态同步以及其他的工作中。定点数的优点就是高度准确,在无法接收有误差的工作中都可以使用它。

5.快速、高效。定点数的运算速度是要高于浮点数。

定点数的实现方式

1.小数定位法

小数定位法其实就是固定浮点数的小数点,比如保留一位,保留两位,这样是其实最接近真实值的。但一般来说没什么人会去用。表面上看加减运算是没什么问题的,但只要一涉及到复杂的乘除运算,弊端就暴露出来了,因为仍是浮点数的原因,所以这些复杂的运算就会产生更多的小数。这种情况下,虽然说能通过技术手段只保留一位小数,但是还是会牺牲准确率。

2.乘法放大入整法

种方法就比较简单粗暴,首先确定放大因子,比如 ‘‘1000’’ 那么所有转为定点数的整型、浮点型数值全都乘上放大因子。
1000”,然后在转为整数。之后的计算就是完全的整型数值之间的运算,完全排除掉了浮点数带来的误差。最后在渲染层需要渲染时,在除上放大因子转为浮点数,交由渲染层去渲染。
这种做法大体来看没什么问题 ,但是细节上会出纰漏。因为放大1000倍,如果在细节上处理不到位,是会丢失掉浮点精度的。如果对定点数理解不深的同学在使用这种方法的情况下,会踩进不小的坑中。

3.位移运算入整法(推荐)

位移运算的原理,下面会讲。
先说一下位移运算的优点。
由于位移运算只能于用作于整形数值,所以非常适合用在定点数中。并且由于位移计算是满足2的次幂的运算的,所以位移运算的效率非常高,速度非常快。

在只计算整形数据的前提下,用位移运算来代替乘除运算简直太好不过。

所以位移运算入整法,是定点数最理想的一种实现方式。

位移运算入整法实现的原理就是提前确定好一个放大次幂,比如 10。那在每个非定点数类型的数值转为定点数时,都将该数值<<(左移)放大次幂10,就能得到一个在原值扩大1024倍的一个数值,也就是定点数。

位移运算的原理

位移运算也成为移位运算。二者皆可,以下就简称为位移运算。

位移运算分为 <<(左移) 和 >>(右移) ,箭头朝左,为左移,箭头朝右为右移。 左移 和 右移 就类似于数学运算中的 乘除
运算,左移可以理解为乘法,右移可以理解为除法。 不过位移运算和乘除还是有一些区别的,位移运算是次幂运算,而乘除运算则是简单的相乘或相除。

下面就以 1 这个数值为案例,进行位移和乘除的操作。为大家讲解何为位移运算,何为乘除运算。

请添加图片描述

由上图可看到,位移运算其实非常简单,就相乘于乘 2 4 8 16 32 …

位移1位就就相当于乘于2,位移2位就相当于乘于4,位移10就相当于乘于1024。
其实也就是 2 的次幂,1位就是2的1次幂,10位就是2的10次幂。

Implicit 隐式转换的原理

1.关键字:Implicit
2.搭配C#关键字 operator 进行使用

隐式转换:不会改变原有数据精确度、引发异常,不会发生任何问题的转换方式。由系统自动转换。
Implicit 关键字使用示例:

		//在float值赋值FinInt值时触发
        public static implicit operator FixInt(float v)
        {
            return new FixInt(v);
        }

隐式转换使用示例:

        int intNum = 1;
        //隐式转换
        long longNum = intNum;
        FixInt fixNum = intNum;

隐式转换可以理解为在数据进行赋值时不需要程序去指定类型或干预的转换,或不同类型可以直接通过=号进行赋值的转换。称之为隐式转换。

如果到这里你还是对隐式转换有疑问,我能保证在看了下面的显示转换之后,你一定会明白什么是隐式转换什么是显示转换。

explicit 显示(强制)转换的原理

1.关键字:explicit
2.搭配C#关键字 operator 进行使用

显示转换:显示转换在开发过程中统称之为强制转换,顾名思义,就是我们程序通过强制指定类型去进行转换的,都可以理解为强制转换。强制转换有可能引发异常、精确度丢失及其他问题的转换方式。需要使用手段进行转换操作。

explicit关键字使用示例:

		//在FixInt值通过(float)转换为float值时触发
        public static explicit operator float(FixInt v)
        {
            return v.RawFloat;
        }

强制转换使用示例:

        double dNum = 1.2321412989585;
        //进行显示(强制)转换操作
        float fNum = (float)dNum;
        int intNum = (int)dNum;

由是强制转换使用案例可以看到,强制转换和隐式转换的区别就在于是否通过 (类型) 指定了要转换的目标类型,如果有通过 (类型) 指定类型,则是强制转换,会触发强制转换接口。如果直接通过 = 进行不同类型的赋值,则是隐式转换,会触发隐式转换的接口。

总结:隐式转换就是不需要添加 (type) 括号+类型进行强转的转换。而显示转换又称为强制转换,需要我们添加 (type) 去指定转换的类型的转换。

定点数实现的原理

定点数的实现原理上面也就讲到过三种方式,小数定位式就不说了,一般来说用的人较少。这里就针对 乘法入整移位入整 进行讲解。

移位运算更快

在说到乘法计算和位移运算的区别的时候,有一点是毋庸置疑的,就是位移运算在计算整形数据时,是要比乘除运算快的。

原因如下:

因为移位指令占2个机器周期,而乘除法指令占4个机器周期。
计算机cpu的移位指令一般单周期就能执行完毕,而其他的指令比如乘法或除法指令都是多周期指令,所以节省了运行时间导致效率更高的结果。

移位运算更准
快是一方面,其实在量体较大的情况下,移位运算是比乘法放大倍数更加准确一点,特别是运算大型数据时。(这里以通常的放大1000倍来举例)。
示例代码:

    void Start()
    {
        int a = 1000;

        float value1 = (long)(a * 1000 * 1.0f / 3400);
        Debug.Log($"乘法扩大 value1:" + value1);

        float value2 = (long)((a << 10) * 1.0f / 3400);
        Debug.Log($"移位运算value2:" + value2);

        Debug.Log( "result float:"+ (value1/100));
        Debug.Log(  "result float:" + (value2/100));
        Debug.Log("result int:" + (int)(value1 / 100));
        Debug.Log("result int:" + (int)(value2 / 100));
    }

示例结果:
在这里插入图片描述
如结果图可见,乘法放大1000倍和位移10位得到的结果是不同的。特别是在经过强转过后,能够很明显的看到,结果完全差了一个数值。这种问题在战斗系统中是非常致命的。
到了这里有人可能就会发现了一点细节问题,说你这计算根本就不准,一个放大1000倍数,一个放大1024倍。中间肯定是有误差的。
这话确实没毛病,确实是有误差的。如果我们使用乘法放大的是1024倍。那么肯定就不会除问题。
所以,综合来说还是放大1000倍,会有问题。其实不管放大多少倍,在我们正常的理解中,2.94转为整形,正确值就应该是3。那这又是为什么转换之后得到的结果却是2呢?

为什么会出现这个问题呢?

这就要从C# int数值的 explicit显示转换 机制说起了,因为Int数值的显示转换是向下取整的。所以我们的2.94数值转换为Int数值之后,结果就会变成2。 这显然不是我们想要的结果。
当然如果这个时候我们使用的放大倍数是1024就不会存在浮点数偏差的问题了。
不过这种方法也可以避免掉,就是在转为Int数值的时候通过 Mathf.Round() 四舍五入接口进行转为Int型,就不会出现数据偏差。但是如果不想数据出现任何偏差,就使用1024作为基准单位,进行放大。

总结:理论上是与1024比较接近,因为我们都知道,1kb=1024字节,为什么1kb不是1000字节呢?而int数值占4个字节。256个int数值就是1024个字节。1024是2的次幂,256也是2的次幂。而计算机不管是移位运算、还是Unity图形压缩格式规范,几乎都是2的次幂有巨大优势。 所以我猜测,计算积底层的计算仍是基于幂运算处理的,只是最小影响化、简略化的给我们转成了以1为单位的数值,因为1.024不管是记起来还是算起来都不怎么方便,也就是舍去了部分精度,得到了最大的便利化。所以,我们只要保证数据放大的基数是进行幂运算,计算机就能保证数据完全准确、速度更快,我们的定点数也就更加准确。

定点数使用注意事项

1.把定点数转换为浮点数后,该浮点数就不应参与帧同步中的任何计算。或者说,帧同步的计算中不需要转换为浮点数,只有把结果交给UI表现时,才需要转为浮点数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铸梦xy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值