浮点数的支持
我们已经在PA3中把仙剑奇侠传运行起来了, 但却不能战斗, 这是因为还有一些浮点数相关的工作需要处理. 现在到了处理的时候了. 要在NEMU中实现浮点指令也不是不可能的事情. 但实现浮点指令需要涉及x87架构的很多细节, 根据KISS法则, 我们选择了一种更简单的方式: 我们通过整数来模拟实数的运算, 这样的方法叫binary scaling.
我们先来说明如何用一个32位整数来表示一个实数.
为了方便叙述, 我们称用binary scaling方法表示的实数的类型为FLOAT
.
我们约定最高位为符号位, 接下来的15位表示整数部分, 低16位表示小数部分,
即约定小数点在第15和第16位之间(从第0位开始).
从这个约定可以看到, FLOAT
类型其实是实数的一种定点表示.
31 30 16 0
+----+-------------------+--------------------+
|sign| integer | fraction |
+----+-------------------+--------------------+
这样, 对于一个实数a
, 它的FLOAT
类型表示A = a * 2^16
(截断结果的小数部分).
例如实数1.2
和5.6
用FLOAT
类型来近似表示, 就是
1.2 * 2^16 = 78643 = 0x13333
+----+-------------------+--------------------+
| 0 | 1 | 3333 |
+----+-------------------+--------------------+
5.6 * 2^16 = 367001 = 0x59999
+----+-------------------+--------------------+
| 0 | 5 | 9999 |
+----+-------------------+--------------------+
而实际上, 这两个FLOAT
类型数据表示的数是:
0x13333 / 2^16 = 1.19999695
0x59999 / 2^16 = 5.59999084
对于负实数, 我们用相应正数的相反数来表示, 例如-1.2
的FLOAT
类型表示为:
-(1.2 * 2^16) = -0x13333 = 0xfffecccd
比较FLOAT和float
FLOAT
和float
类型的数据都是32位, 它们都可以表示2^32个不同的数.
但由于表示方法不一样, FLOAT
和float
能表示的数集是不一样的.
思考一下, 我们用FLOAT
来模拟表示float
, 这其中隐含着哪些取舍?
接下来我们来考虑FLOAT
类型的常见运算, 假设实数a
, b
的FLOAT
类型表示分别为A
, B
.
- 由于我们使用整数来表示
FLOAT
类型,FLOAT
类型的加法可以直接用整数加法来进行:A + B = a * 2^16 + b * 2^16 = (a + b) * 2^16
- 由于我们使用补码的方式来表示
FLOAT
类型数据, 因此FLOAT
类型的减法用整数减法来进行.A - B = a * 2^16 - b * 2^16 = (a - b) * 2^16
FLOAT
类型的乘除法和加减法就不一样了:
也就是说, 直接把两个A * B = a * 2^16 * b * 2^16 = (a * b) * 2^32 != (a * b) * 2^16
FLOAT
数据相乘得到的结果并不等于相应的两个浮点数乘积的FLOAT
表示. 为了得到正确的结果, 我们需要对相乘的结果进行调整: 只要将结果除以2^16
, 就能得出正确的结果了. 除法也需要对结果进行调整, 至于如何调整, 当然难不倒聪明的你啦.- 如果把
A = a * 2^16
看成一个映射, 那么在这个映射的作用下, 关系运算是保序的, 即a <= b
当且仅当A <= B
, 故FLOAT
类型的关系运算可以用整数的关系运算来进行.
有了这些结论, 要用FLOAT
类型来模拟实数运算就很方便了.
除了乘除法需要额外实现之外, 其余运算都可以直接使用相应的整数运算来进行. 例如
float a = 1.2;
float b = 10;
int c = 0;
if (b > 7.9) {
c = (a + 1) * b / 2.3;
}
用FLOAT
类型来模拟就是
FLOAT a = f2F(1.2);
FLOAT b = int2F(10);
int c = 0;
if (b > f2F(7.9)) {
c = F2int(F_div_F(F_mul_F((a + int2F(1)), b), f2F(2.3)));
}
其中还引入了一些类型转换函数来实现和FLOAT
相关的类型转换.
仙剑奇侠传的框架代码已经用FLOAT
类型对浮点数进行了相应的处理.
你还需要实现一些和FLOAT
类型相关的函数:
/* navy-apps/apps/pal/include/FLOAT.h */
int32_t F2int(FLOAT a);
FLOAT int2F(int a);
FLOAT F_mul_int(FLOAT a, int b);
FLOAT F_div_int(FLOAT a, int b);
/* navy-apps/apps/pal/src/FLOAT/FLOAT.c */
FLOAT f2F(float a);
FLOAT F_mul_F(FLOAT a, FLOAT b);
FLOAT F_div_F(FLOAT a, FLOAT b);
FLOAT Fabs(FLOAT a);
其中F_mul_int()
和F_div_int()
用于计算一个FLOAT
类型数据和一个整型数据的积/商,
这两种特殊情况可以快速计算出结果, 不需要将整型数据先转化成FLOAT
类型再进行运算.
事实上, 我们并没有考虑计算结果溢出的情况,
不过仙剑奇侠传中的浮点数结果都可以在FLOAT
类型中表示, 所以你可以不关心溢出的问题.
如果你不放心, 你可以在上述函数的实现中插入assertion来捕捉溢出错误.
实现binary scaling
实现上述函数来在仙剑奇侠传中对浮点数操作进行模拟. 实现正确后, 你就可以在仙剑奇侠传中成功进行战斗了.