避开浮点数——伪浮点数

来源:互联网 发布:如何下载visio软件 编辑:程序博客网 时间:2024/05/21 06:17

对于定点处理器来说,浮点数学运算的代价是非常巨大的(大多数嵌入式应用都是定点处理器)。相对于整数数学运算,浮点数学运算慢的要死。每个操作都会调用某个库来实现相应的函数,而这个库通常相当大。即使所使用的浮点数学非常简单,应用程序的大小也是非常惊人的。

(标准的输入输出函数(例如printfiostream)通常包含浮点处理。即使代码中没有使用浮点,但如果使用了这些函数,那么代码还是会包含浮点算术库。)

除非有很好的理由,否则就应该像躲避瘟疫一样避开浮点数。如果不能避开,可以用伪装。

 

先介绍二级制换算,这是后面的基础。

除法运算是个成本相对较高的步骤,但是如果是除以常数,则有些方法可以解决这个问题,我们可以将除法用位运算来近似。

Input/6为例。

Input/6如果将分子和分母都放大同样大的倍数,然后将分母换成离它最近的能用2的幂表示的数,则分母可以写成2的多少次方,然后就可以换成移位运算了。

 

1:2的幂为除数近似1/6

乘数

除数

等价移位

结果

误差%

1

6

0.166 666 667

0

3

16

4

0.1875

12.5

5

23

5

0.156 25

6.2

11

64

6

0.171 875

3.1

21

128

7

0.164 063

1.5

43

256

8

0.167 969

0.78

85

512

9

0.166 016

0.39

171

1024

10

0.166 992

0.19

341

2048

11

0.166 504

0.09

683

4096

12

0.166 748

0.04

注意,每多一次移位,这个表中误差就减半。

#define DIVIDE_THREE_FACT_MULT 171

#define DIVIDE_THREE_FACT_SHIFT 10

 

int16_t DivideThreeFactorial(int16_t  input){

int32_t  tmp = input*DIVIDE_THREE_FACT_MULT;

return  (tmp >> DIVIDE_THREE_FACT_SHIFT);

}

注意,输入和输出都是16位,但是为了存放乘法的结果必须要用32位的变量。即使使用了较大的临时变量,这个函数也比除法成本低。

通过这种近似会降低精度。

12.345可以表示为49/4,误差为0.095。使用更大的分母移位值,我们能得到更大的精度。但是,分母太大会导致分子溢出

2:用二进制换算表示数值12.345

分子

分子需要的位数

分母移位值

等效浮点数

误差

12

4

0

12

0.345

25

5

1

12.5

0.155

99

7

3

12.375

0.030

395

9

5

12.343 75

0.001 25

126 41

14

10

12.344 726 56

0.000 273

12 944 671

24

20

12.345 000 27

2.67E-07

414 229 463

29

25

12.345

1.19E-09

1 656 917 852

31

27

12.345

1.19E-09

因此,在分母中做移位可以减小误差,但同时也增加了分子需要的位数。这又将我们带回到两个问题:对这个算数运算,系统可以忍受多大的误差?经过算法中的乘法和加法运算之后期望的结果值有多大?

 

伪浮点数

回忆小时候,我们还不知道什么叫浮点数,但是我们知道0.25,可以写成1/4。任何有理数都可以写成分数。即使无理数也可以近似用分数表示,而且分母大点儿误差就会更小。例如,π通常近似成22/7

我们现在想要避开浮点数,所以想到用分数表示,但是除法的代价也不低,所以可以用上面提到的二进制换算。

定义伪浮点数

struct  sFakeFloat{

int32_t  num; //numerator

int8_t shift; //right-shift value (use negative for left-shift)

}

这个结构中存放的数表示:

floatingPointValue = num/2^shift    (数学意义)

floatingPointValue = num >> shift;   //in the actual code

负的移位值示意移位方向的改变

struct  sFakeFloat four = { 1, -2 };

floatingPointValue = four.num >> shift ==> 1/(2^-2)==>1*2^2==>4

 

 

加法(和减法)

struct  sFakeFloat{

Int8_t  num;

int8_t shift;

}

 

举例:

struct  sFakeFloat a = { 99, 3 }; //12.375, not quite 12.345

struct  sFakeFloat a = { 111, 5 }; //3.46875, not quite 3.456

struct  sFakeFloat result; //15.84375, close to ideal of 15.831

 

int16_t  tmp = a.num;

tmp = tmp << ( b.shift - a.shift );

这里必须使用更大的临时变量,否则结果可能溢出并被截断。注意,分子并不是唯一会发生溢出的部分。分母必须小于8位,为了避免溢出,除了对较小的分母做移位放大外,还需要对较小的分母做移位缩小(除以2,精度减小)。

分子的和存放在临时变量中,直到我们确信它足够小,能够放进结果中。

tmp = tmp + b.num;

此处,移位值取b.shift=5,临时变量是507,太大了,不能用8位表示。需要将分子变小。

result.shift = b.shift;

while( tmp > INT8_MAX  ||   tmp < -INT8_MAX){

tmp = tmp >> 1;

Result.shift--;

}

 

 

乘法(和除法)

乘法比加法要简单一些,因为不需要做分母分配。对于乘法,只需将分子和分母分别相乘就得到结果(2/3 x 5/3 = 10/9.

首先将一个数的分子放进临时变量中,以防止发生溢出的错误。加法需要检查结果的大小,而乘法不用,可以坑定两个32位变量的乘法不会超过64位。然后用第二个数的分子乘以这个临时变量。(注意64位整数可能并非是处理器内置的数据类型,因此使用会产生一些额外开销,但比使用浮点型(float或则double)变量产生的额外的开销少多了)

和加法一样,只要分子相乘的结果不能放进结果的分子中,就需要改变移位值来避免溢出。

 

确定误差

要想结果最优,对于将要处理的数字需要预先了解,这样才能够处理溢出,保证系统的稳定性。可以创建一个非常通用的库来处理每一个可能的情况,但这样做的风险是需要重新实现浮点数。在评判算法的时候,需要知道变量的范围以及在每个时间处理变量所需要的精度

以满足系统要求为目的,进行误差检查,然后改变一些参数,继续迭代检查,使之达到要求。

1.决定分子需要有多少位。

2.选择一个移位值,使分子能放进可用的位数中。注意是否有一个可接受的移位值范围。

3.确认数据的极端值可以用二进制换算的格式表示而不会发生溢出。

4.确认粒度是可以接受的,即应该小于目标误差。

5.检查极端值和额定值的误差。这些误差应该总是比粒度小,但能够提供对换算值是否有效的另一种检查机制。

0 0
原创粉丝点击