抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

在某些情况下,我们需要保证同样的输入得到同样的结果。因此我们要么使用整形,要么就使用定点数。

本文主要介绍定点数的原理。

什么是定点数

定点数是指小数点位置固定的数,用整数来表示小数。所以本质上,定点数的运算其实就是整数的运算。由于浮点数使用的是科学计数法来表示,还原成数字的时候会产生误差,因此使用整数运算的定点数不会产生精度误差。

定点数原理

我们约定定点数的名字叫做 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 能表示的最大位数,则该结果溢出,所以我们需要进行溢出判断。

溢出判断主要是对符号位进行检测。

  1. 首先我们通过异或判断 xl ^ yl 两个参数的符号位是否相同,相同情况得到的结果,符号位为 0,否则为 1。

  2. 然后我们对该结果取反,即我们检测两个参数为同号的情况。因为只有同号相加的情况,才会产生溢出,异号相加的结果只会在正负极限值之内。

  3. 接着我们判断 xl ^ sum 参数 1 与结果的符号是否相同(也可以用参数 2)。

  4. 根据前面的结果,我们接下来判断 (~(xl ^ yl) & (xl ^ sum)) 符号位是否相同。这里会出现两种结果:

    1. 两个参数同号:~(xl ^ yl) 符号位为 1。由于溢出会使符号相反,因此如果 (xl ^ sum) 同号,即为 0,不溢出,则在符号位上,1 & 0 = 0;如果(xl ^ sum) 异号,即为 1,溢出,则在符号位上,1 & 1 = 1
    2. 两个参数异号:~(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 能表示的最大位数,则该结果溢出,所以我们需要进行溢出判断。

溢出判断主要是对符号位进行检测。

  1. 首先我们通过异或判断 xl ^ yl 两个参数的符号位是否相同,相同情况得到的结果,符号位为 0,否则为 1。

  2. 此处我们不对该结果取反,和加法相反。因为只有异号相减的情况,才会产生溢出,同号相减的结果只会在正负极限值之内。

  3. 接着我们判断 xl ^ sum 参数 1 与结果的符号是否相同(也可以用参数 2)。

  4. 根据前面的结果,我们接下来判断 ((xl ^ yl) & (xl ^ sum)) 符号位是否相同。这里会出现两种结果:

    1. 两个参数异号:(xl ^ yl) 符号位为 1。由于溢出会使符号相反,因此如果 (xl ^ sum) 同号,即为 0,不溢出,则在符号位上,1 & 0 = 0;如果(xl ^ sum) 异号,即为 1,溢出,则在符号位上,1 & 1 = 1
    2. 两个参数同号:(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;
}

/// <summary>
/// 只在同号情况下有用,计算是否溢出
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="overflow"></param>
/// <returns></returns>
static long AddOverflowHelper(long x, long y, ref bool overflow)
{
var sum = x + y;
// x + y overflows if sign(x) ^ sign(y) != sign(sum)
overflow |= ((x ^ y ^ sum) & Min_Value) != 0;
return sum;
}

我们重载除法运算符。

除法的运算是模拟我们数学上除法的过程。

如果除数是 0,直接返回最大值。

我们定义余数 remainder,除数 divider,商 quotient,计算位数 bitPos。

余数初始化为参数 1,除数初始化为参数 2,商初始化为 0,计算位数初始化为 33 位。

然后我们开始计算:

  1. 先判断小数部分有没有多余的 0,此处选择每次右移 4 位(2 的 4 次幂),即一个十六进制 0。这样可以快速移动到最小计算位数。
  2. 然后我们开始循环,如果有余数并且计算位数大于等于 0 的情况,我们就进行一次除法。
    1. 我们先获得余数前导零个数,然后和需要计算的位数进行比较,防止溢出。
    2. 然后我们左移余数,这一步是为了防止在循环的过程中余数不够位数参与运算。同时我们剩余的计算位数要减去左移的位数。
    3. 接着我们求商和余数,新的余数赋值给原来的余数,商根据左移位数同样移动并且加到总商上。
    4. 判断结果有没有溢出。
    5. 余数进位,继续循环。
  3. 商 +1 并右移一位,进行舍入。
  4. 最后判断是否异号,是的话符号位就取反。
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;
//throw new DivideByZeroException();
}

var remainder = (ulong)(xl >= 0 ? xl : -xl);
var divider = (ulong)(yl >= 0 ? yl : -yl);
var quotient = 0UL;
var bitPos = Num_Bit / 2 + 1;

//加速除法,减少小数部分多余的0数量
// If the divider is divisible by 2^n, take advantage of it.
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;

// Detect overflow
//0xFFFFFFFFFFFFFFFF >> bitPos得到64-bitPos个1
//取反得到11100....(64-bitPos个0)
//如果结果不为0,说明超出最大位数,溢出
if ((div & ~(0xFFFFFFFFFFFFFFFF >> bitPos)) != 0)
{
return ((xl ^ yl) & Min_Value) == 0 ? MaxValue : MinValue;
}
//前进一位
remainder <<= 1;
--bitPos;
}

// rounding
++quotient;
var result = (long)(quotient >> 1);
if (((xl ^ yl) & Min_Value) != 0)
{
result = -result;
}

FP r;
r._value = result;
return r;
//return new FP(result);
}

/// <summary>
/// 求前导0个数
/// </summary>
/// <param name="x"></param>
/// <returns></returns>
public static int CountLeadingZeroes(ulong x)
{
int result = 0;
//如果最高四位(二进制)都为0,则左移四位,加速计算
while ((x & 0xF000000000000000) == 0) { result += 4; x <<= 4; }
//如果最高一位(二进制)为0,则左移一位
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;
// 判断是否为特殊情况:被除数是最小负数且除数是-1
// 这种情况下整数溢出会导致结果为0
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;
}

更新日志

2024-01-28

  1. 补上引用内容

2024-01-28

  1. 更新基础内容。

评论