目录

【转载】定点数杂谈

注:本文转载自 定点数杂谈 - 掘金

如有侵权,联系删除

假如你开发了一个后台系统,其中有关于金额的一些操作。比如说,最小精度是,也就是一分钱。

那么可以规定,1 代表一分,而10代表一角,自然100代表一元钱。

此时此刻,0.1元这个金额就可以用 10 这个整数来表示。

这就代表着,用整数类型的变量是完全可以存储一个小数类型的数值的。

为什么要用整数来存储小数

因为现在计算机里自带的浮点数是不精确的。这个跟浮点数的存储方式有关系。这并不是本文的主要内容。所以这里不详解了。

用两个整数来表示一个小数

既然不能用浮点数,那么用两个整数来表达任意有理数。也就是分数的概念:

  • 分子 : 一个任意整数(不能超出计算机能表示的范围)
  • 分母 : 一个任意整数(不能超出计算机能表示的范围)

示例代码(Golang):

1
2
3
4
type MyNumber struct {
    Fenzi int64 // 分子
    Fenmu int64 // 分母
}

上述代码中,如果我们要表达 1/3, 那么

1
2
3
4
var n = MyNumber{
    Fenzi : 1,
    Fenmu : 3,
}

这样一来,只要不超过int64的范围,那么任意有理数都能被表示出来了。

但是有下面的问题:

  • 如何来进行加减乘除的运算?
  • 如何导出成正规的人能看得懂的数字呢?

答案就是,自己实现这些操作。

这里给出一个加法的例子(请复习小学数学):

x/y+a/b=(xb+ay)/yb

1
2
3
4
5
6
7
8
// myNumber = myNumber + other
// 加法
func (myNumber *MyNumber) Add (other *MyNumber) {
    newFenzi := myNumber.Fenzi * other.Fenmu + myNumber.Fenmu * other.Fenzi
    newFenmu := myNumber.Fenmu * other.Fenmu
    myNumber.Fenzi = newFenzi
    myNumber.Fenmu = newFenmu
}

那么其他的运算啥的跟上面同样,用小学数学知识就行了。

这样搞行不行呢,当然行。

不过,这样搞有点类似于 BigDecimal,就是大数。

注意:真正的大数能超脱int64, 而上面的实现,还是基于int64的。

开始讲定点数

定点数本身的类型可以是int32, 也可以是int64,这取决于你的系统需要多大范围的数。

对,定点数本身不是BigDecimal,无法表示任意有理数,定点数是有范围的。

举一个例子:

int26_6 : 本质类型是 int32,只不过,前面26位bit,用来存放整数部分,后面6位用来存放小数部分。

那么具体怎么存放的呢,举一个例子:

1.25=1+1/4

如上,我们将 1.25 分成 11/4两个部分。

1,我们用 1<<6 = 64来表示。 1/4, 我们用 1<<4 = 16来表示。 1+1/4, 自然就是上述两者相加: 1<<6 + 1<<4 = 64 + 16 = 80来表示。

用二进制表示:

0000 0000 0000 0000 0000 0000 01|01 0000

注意上面的竖线,表示整数和小数之间的小数点。

这里有一个问题,前面整数部分很好理解,最低位是1,那么整数部分整个就是 1

小数部分呢:

010000 为什么就是 1/4?

我们看一下 010000 这个二进制代表的是几:16。

而 四个16正好就是 64, 记得吗,64 在我们的算法里,就是整数1。

我们在二进制里也来计算一下四个010000相加:

010000 + 010000 + 010000 + 010000 = 1000000

1000000在我们的算法里,也正好就是整数1

从上面可以看出来,int26_6的加减法,正好兼容了原本整数的加减法,无需做改造。

但是乘除就不行了。这里不详解,有兴趣的可以去网上查,也可以自己推导。

用定点数的好处

  • 精确 (尤其涉及到金额时, 当然金额最好用BigDecimal方案)
  • 多系统同步(如果你做过游戏,就明白了)
  • 比浮点数计算更节省cpu