简介
在某些情况下,我们需要保证同样的输入得到同样的结果。因此我们要么使用整形,要么就使用定点数。
本文主要介绍定点数的原理。
什么是定点数
定点数是指小数点位置固定的数,用整数来表示小数。所以本质上,定点数的运算其实就是整数的运算。由于浮点数使用的是科学计数法来表示,还原成数字的时候会产生误差,因此使用整数运算的定点数不会产生精度误差。
定点数原理
我们约定定点数的名字叫做 FP。
定点数表示
本文定点数用 long 长整型来表示。长整型一共 64 位,最高位表示符号,除符号位外,前 31 位表示整数部分,后 32 位表示小数部分。
定点数运算
首先我们定义一个基本单位,用于计算。
1 2
| public const int FRACTIONAL_PLACES = 32; public const long ONE = 1L << FRACTIONAL_PLACES;
|
ONE 代表 FP 的一个单位大小,所以我们在类型转换的时候通过原来的数值大小乘以 ONE,就可以得到转变为 FP 类型之后的值。
在二进制的视角下,就相当于原来的数左移了 32 位。
浮点数转换的时候需要显示转换为 long。
从 FP 转换到其他类型的话只要进行相反的操作,即除以 ONE,然后如果是 int 类型就在计算完后显示转换为 int,如果是浮点类型就提前显示转换为浮点类型即可。
通过重载类型转换,我们可以在使用的时候更方便地进行类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public FP(long value) { _value = value; }
public static implicit operator FP(long value) { FP res; res._value = value * ONE; return res; }
public static implicit operator FP(int value) { FP res; res._value = value * ONE; return res; }
public static implicit operator FP(float value) { FP res; res._value = (long)(value * ONE); return res; }
public static implicit operator FP(double value) { FP res; res._value = (long)(value * ONE); return res; }
public static explicit operator int(FP fp) { return (int)(fp._value / ONE); }
public static explicit operator long(FP fp) { return fp._value >> FRACTIONAL_PLACES; }
public static explicit operator float(FP fp) { return (float)fp._value / ONE; }
public static explicit operator double(FP fp) { return (double)fp._value / ONE; }
|
我们只要通过如下方法,就可以得到 FP 数:
1 2
| FP num = 1; FP num2 = 1.1f;
|
我们重载加法运算符。
加法只要把两个 FP 的值相加,得到一个新的值即可。
如果相加之后出现超出 FP 能表示的最大位数,则该结果溢出,所以我们需要进行溢出判断。
溢出判断主要是对符号位进行检测。
首先我们通过异或判断 xl ^ yl
两个参数的符号位是否相同,相同情况得到的结果,符号位为 0,否则为 1。
然后我们对该结果取反,即我们检测两个参数为同号的情况。因为只有同号相加的情况,才会产生溢出,异号相加的结果只会在正负极限值之内。
接着我们判断 xl ^ sum
参数 1 与结果的符号是否相同(也可以用参数 2)。
根据前面的结果,我们接下来判断 (~(xl ^ yl) & (xl ^ sum))
符号位是否相同。这里会出现两种结果:
- 两个参数同号:
~(xl ^ yl)
符号位为 1。由于溢出会使符号相反,因此如果 (xl ^ sum)
同号,即为 0,不溢出,则在符号位上,1 & 0 = 0
;如果(xl ^ sum)
异号,即为 1,溢出,则在符号位上,1 & 1 = 1
。
- 两个参数异号:
~(xl ^ yl)
符号位为 0,则符号位结果必为 0。
5.最后我们跟 Min_Value 求与,Min_Value 是 float 的最小数,即最高位符号位为 1,其余都是 0。这样就能单独比较符号位的结果是否为 0,不是则溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static FP operator +(FP x,FP y) { var xl = x._value; var yl = y._value; var sum = xl + yl; if (((~(xl ^ yl) & (xl ^ sum)) & Min_Value) != 0) { sum = xl > 0 ? Max_Value : Min_Value; } FP result; result._value = sum; return result; }
|
我们重载减法运算符。
加法只要把两个 FP 的值相减,得到一个新的值即可。
如果相加之后出现超出 FP 能表示的最大位数,则该结果溢出,所以我们需要进行溢出判断。
溢出判断主要是对符号位进行检测。
首先我们通过异或判断 xl ^ yl
两个参数的符号位是否相同,相同情况得到的结果,符号位为 0,否则为 1。
此处我们不对该结果取反,和加法相反。因为只有异号相减的情况,才会产生溢出,同号相减的结果只会在正负极限值之内。
接着我们判断 xl ^ sum
参数 1 与结果的符号是否相同(也可以用参数 2)。
根据前面的结果,我们接下来判断 ((xl ^ yl) & (xl ^ sum))
符号位是否相同。这里会出现两种结果:
- 两个参数异号:
(xl ^ yl)
符号位为 1。由于溢出会使符号相反,因此如果 (xl ^ sum)
同号,即为 0,不溢出,则在符号位上,1 & 0 = 0
;如果(xl ^ sum)
异号,即为 1,溢出,则在符号位上,1 & 1 = 1
。
- 两个参数同号:
(xl ^ yl)
符号位为 0,则符号位结果必为 0。
5.最后我们跟 Min_Value 求与,Min_Value 是 float 的最小数,即最高位符号位为 1,其余都是 0。这样就能单独比较符号位的结果是否为 0,不是则溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static FP operator -(FP x, FP y) { var xl = x._value; var yl = y._value; var diff = xl - yl; if ((((xl ^ yl) & (xl ^ diff)) & Min_Value) != 0) { diff = xl > 0 ? Max_Value : Min_Value; } FP result; result._value = diff; return result; }
|
我们重载乘法运算符。
乘法运算我们分为两个部分,四个步骤进行。两个部分是整数部分和小数部分,四个步骤,是两个参数的整数和小数分别相乘。最后我们把得到的结果相加,就是乘法的结果了。
我们可以类比为竖式计算的过程,这样或许会更好理解。
比如 $1.1 \times 1.1$ ,我们根据规则得到以下四个式子:
$0.1 \times 0.1 = 0.01$
$1 \times 0.1 = 0.1$
$1 \times 0.1 = 0.1$
$1 \times 1 = 1$
最后把结果加起来就是 $0.01+0.1+0.1+1=1.21$
我们对小数部分与0x00000000FFFFFFFF
求与,得到小数部分表示为整数的值;对整数部分右移 32 位,得到正确的整数值(而不是定点数表示的整数值)。
小数部分相乘的情况下,得到的结果会变成 64 位,因此我们需要右移 32 位使其归位。
整数部分相乘的情况下,得到的结果左移 32 位,使其回到定点数表示的整数位置。
小数与整数相乘的情况下,不会使小数位数产生变化,进位的值也会自动进入整数位置,因此不需要进行操作。
接着我们逐个相加,每次相加都判断是否有溢出。
这里判断溢出的方式和加法不同,这里只需要判断同号的情况,因为我们已经把两个参数转为同号,所以只要判断两个参数和得到的结果是否同号即可。
最后我们通过之前判断的两个参数是否同号,决定乘法结果的正负。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| public static FP operator *(FP x, FP y) { var xl = x._value; var yl = y._value;
bool opSignsEqual = ((xl ^ yl) & Min_Value) == 0;
if (xl < 0) { xl = -xl; } if(yl < 0) { yl = -yl; }
var xlo = (xl & 0x00000000FFFFFFFF); var xhi = xl >> FRACTIONAL_PLACES; var ylo = (yl & 0x00000000FFFFFFFF); var yhi = yl >> FRACTIONAL_PLACES;
var lolo = xlo * ylo; var lohi = xlo * yhi; var hilo = xhi * ylo; var hihi = xhi * yhi;
var loResult = lolo >> FRACTIONAL_PLACES; var midResult1 = hilo; var midResult2 = lohi; var hiResult = hihi << FRACTIONAL_PLACES;
bool overflow = false; var sum = AddOverflowHelper(loResult, midResult1, ref overflow); sum = AddOverflowHelper(sum, midResult2, ref overflow); sum = AddOverflowHelper(sum, hiResult, ref overflow);
if(overflow) { sum = Max_Value; } if (!opSignsEqual) { sum = -sum; }
FP result; result._value = sum; return result; }
static long AddOverflowHelper(long x, long y, ref bool overflow) { var sum = x + y; overflow |= ((x ^ y ^ sum) & Min_Value) != 0; return sum; }
|
我们重载除法运算符。
除法的运算是模拟我们数学上除法的过程。
如果除数是 0,直接返回最大值。
我们定义余数 remainder,除数 divider,商 quotient,计算位数 bitPos。
余数初始化为参数 1,除数初始化为参数 2,商初始化为 0,计算位数初始化为 33 位。
然后我们开始计算:
- 先判断小数部分有没有多余的 0,此处选择每次右移 4 位(2 的 4 次幂),即一个十六进制 0。这样可以快速移动到最小计算位数。
- 然后我们开始循环,如果有余数并且计算位数大于等于 0 的情况,我们就进行一次除法。
- 我们先获得余数前导零个数,然后和需要计算的位数进行比较,防止溢出。
- 然后我们左移余数,这一步是为了防止在循环的过程中余数不够位数参与运算。同时我们剩余的计算位数要减去左移的位数。
- 接着我们求商和余数,新的余数赋值给原来的余数,商根据左移位数同样移动并且加到总商上。
- 判断结果有没有溢出。
- 余数进位,继续循环。
- 商 +1 并右移一位,进行舍入。
- 最后判断是否异号,是的话符号位就取反。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| public static FP operator /(FP x, FP y) { var xl = x._value; var yl = y._value;
if (yl == 0) { return Max_Value; }
var remainder = (ulong)(xl >= 0 ? xl : -xl); var divider = (ulong)(yl >= 0 ? yl : -yl); var quotient = 0UL; var bitPos = Num_Bit / 2 + 1;
while ((divider & 0xF) == 0 && bitPos >= 4) { divider >>= 4; bitPos -= 4; } while (remainder != 0 && bitPos >= 0) { int shift = CountLeadingZeroes(remainder); if (shift > bitPos) { shift = bitPos; } remainder <<= shift; bitPos -= shift; var div = remainder / divider; remainder = remainder % divider; quotient += div << bitPos;
if ((div & ~(0xFFFFFFFFFFFFFFFF >> bitPos)) != 0) { return ((xl ^ yl) & Min_Value) == 0 ? MaxValue : MinValue; } remainder <<= 1; --bitPos; }
++quotient; var result = (long)(quotient >> 1); if (((xl ^ yl) & Min_Value) != 0) { result = -result; }
FP r; r._value = result; return r; }
public static int CountLeadingZeroes(ulong x) { int result = 0; while ((x & 0xF000000000000000) == 0) { result += 4; x <<= 4; } while ((x & 0x8000000000000000) == 0) { result += 1; x <<= 1; } return result; }
|
我们重载求余运算符。
求余只增加一种情况判定,防止溢出,其他和原本的求余一样。
1 2 3 4 5 6 7 8 9 10
| public static FP operator %(FP x, FP y) { FP result; result._value = x._value == Min_Value & y._value == -1 ? 0 : x._value % y._value; return result; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public static FP operator -(FP x) { return x._value == Min_Value ? MaxValue : new FP(-x._value); }
public static bool operator ==(FP x, FP y) { return x._value == y._value; }
public static bool operator !=(FP x, FP y) { return x._value != y._value; }
public static bool operator >(FP x, FP y) { return x._value > y._value; }
public static bool operator <(FP x, FP y) { return x._value < y._value; }
public static bool operator >=(FP x, FP y) { return x._value >= y._value; }
public static bool operator <=(FP x, FP y) { return x._value <= y._value; }
|
更新日志