大多数CPU都有硬件的浮点单元(FPU),但是有一些MCU内核(比如Cortex-M3)没有FPU,或者只支持单精度,同时大部分CPU都不支持高精度128位的浮点数,如果需要使用这些,则需要软件进行模拟(即使用CPU的定点运算指令),使用软件模拟而不用硬件单元的浮点数通常叫做软浮点数。
本文就以IEEE-754单精度浮点数为例,实现软浮点数的一些基本运算操作。我最早是为了FPC的微控制器开发实现一个精简的单精度软浮点模块,后来遇到了不少坑,花了不少时间才完全实现,所以就记录一下,考虑到大多数人不熟悉Pascal,所以用C重写了一遍。本文的代码不是最优化的,主要是为了一定的代码可读性,同时使生成代码的体积尽可能小。
其实网上可以找到一个非常好的软浮点实现,http://www.jhauser.us/arithmetic/SoftFloat.html,但是其代码比较复杂,很多非常高水平的操作,我也看不懂很多东西。本文的代码有部分内容(主要是比较运算)参考了其实现。
本文代码实现了两种舍入模式,即向0舍入(截断)和向最近的偶数舍入这两种。默认使用向偶数舍入,通过在include头文件之前定义宏#define ROUND_TRUNC可以改成向0舍入,通过降低运算精度减小代码体积(对于一些MCU来说)。
考虑到非规格化数并不常用,且会增加代码复杂度,本文代码忽略了非规格化数的操作,在运算中一律视为0。
完整代码在https://gitee.com/peazomboss/softfloat32可以找到,使用MIT许可证。
注意:代码仅使用gcc编译测试,不确定其他C或C++编译器。
在IEEE-754标准之前,各厂商有自己的浮点格式,但是现在IEEE-754已经一统江湖了,目前最新的标准是IEEE-754 2008,不过这个标准的完整文件是要收钱的,很贵哦,当然咱也没必要看,因为网上可以找到大量资料。
简单来说,目前最常用的是单精度浮点数和双精度浮点数,在AI领域还有NV的半精度浮点数,以及一些科学计算需要的128位浮点数。当然还有80位扩展精度浮点数之类的,但是现在基本不常见了。
以32位单精度浮点数为例,其由1位符号(Sign),8位指数(Exponent)和23位有效数(Significand)组成。其中指数也有叫阶码的(本文叫指数),有效数也有叫尾数的(本文不作区分)。
其中8位指数是用偏移量表示的,所以指数127相当于实际指数0,指数取值为1~254,其中0和255有特殊用途;23位有效数是规格化(Normalized,也有叫标准化,规约化的)的结果,并不包含隐含的1,所以实际上其有效数有24位。
其他的我也不多说了,网上讲的好的非常多,我不干这些重复的事了。
简单说例如数字52,可以表示为1.101×2^5(即1.625*32),那么其符号为0,指数为5+127=132,有效数为1.101,规格化后的结果为0 10000100 1010000000...,基本就是这样。对于其他精度的浮点数,也就是指数和有效数的不同。
头文件softfloat32.h基本内容如下:
#include <stdint.h>
#include <stdbool.h>
typedef uint32_t sfloat32;
// 将浮点数转换到软浮点数
sfloat32 sf32_from_float(float f);
// 将软浮点数转换到浮点数
float sf32_to_float(sfloat32 sf);
// 判断是否是NaN
bool sf32_isnan(sfloat32 sf);
// 判断是否无穷
bool sf32_isinf(sfloat32 sf);
// 获取符号位
bool sf32_sign(sfloat32 sf);
其中在softfloat32.c文件中部分内容如下:
#include "softfloat.h"
#define SF32_INF 0x7f800000 // Inf,指数为0xff,有效位为0
#define SF32_NAN 0xffc00000 // NaN,指数为0xff,有效位不为0
#define SF32_MAX 0x7f7fffff // 单精度浮点表示的最大值
#define SF32_EXP(sf) (((sf) >> 23) & 0xff) // 取8位指数
#define SF32_SIG(sf) ((sf) & 0x7fffff) // 取23位有效位
#define SF32_SIGN(sf) ((sf) >> 31) // 取符号位
/* 构造一个32位浮点数 */
static inline sfloat32 sf32_pack(bool sign, uint16_t exp, uint32_t sig)
{
return ((uint32_t)sign << 31) | ((uint32_t)exp << 23) | (sig & 0x7fffff);
}
inline sfloat32 sf32_from_float(float f)
{
union
{
float f;
uint32_t u;
} usf;
usf.f = f;
return usf.u;
}
inline float sf32_to_float(sfloat32 sf)
{
union
{
float f;
uint32_t u;
} usf;
usf.u = sf;
return usf.f;
}
bool sf32_isnan(sfloat32 sf)
{
return (sf << 9 != 0) && (((sf >> 23) & 0xff) == 0xff);
}
bool sf32_isinf(sfloat32 sf)
{
return (sf & 0x7fffffff) == SF32_INF;
}
bool sf32_sign(sfloat32 sf)
{
return SF32_SIGN(sf);
}
这些是基本的操作,使用union进行变量类型的转换可以一定程度上避免ub。
把比较运算放到前面,是因为这个实现起来非常简单。由于IEEE-754的指数是按照正常值加一个偏置值形成的,因为8位的指数在高位,而尾数在低位,所以除了一些特殊情况,按照定点数的规则即可比大小。
特殊情况是指NaN,任何数和NaN比较,只有不等于返回true,其他全是false,这个规则比较重要。
具体的函数声明如下:
// 判断相等
bool sf32_eq(sfloat32 sf1, sfloat32 sf2);
// 判断不相等
bool sf32_ne(sfloat32 sf1, sfloat32 sf2);
// 判断小于
bool sf32_lt(sfloat32 sf1, sfloat32 sf2);
// 判断小于等于
bool sf32_le(sfloat32 sf1, sfloat32 sf2);
// 判断大于
bool sf32_gt(sfloat32 sf1, sfloat32 sf2);
// 判断大于等于
bool sf32_ge(sfloat32 sf1, sfloat32 sf2);
以下是相等和不相等的实现,对于前面NaN的规则来说,不相等就是相等的简单取反:
bool sf32_eq(sfloat32 sf1, sfloat32 sf2)
{
if (sf32_isnan(sf1) || sf32_isnan(sf2))
return false;
return
(sf1 == sf2) || // 内容完全一样
(((sf1 | sf2) << 1) == 0); // 对于0则忽略符号位
}
bool sf32_ne(sfloat32 sf1, sfloat32 sf2)
{
return !sf32_eq(sf1, sf2);
}
但是对于其他操作数来说,则不是这样,比如大于不是小于等于的简单取反,而是要单独判断NaN,所以下面的代码也就好理解了:
static bool sf32_real_lt(sfloat32 sf1, sfloat32 sf2)
{
bool sign1 = SF32_SIGN(sf1);
bool sign2 = SF32_SIGN(sf2);
if (sign1 != sign2) // 符号不同,也可以用异或判断
// 若左边为负数且两数不为0,则说明左边小于右边
return sign1 && ((sf1 | sf2) << 1);
// 若符号相同,则左边小于右边的条件是两数不相等
// 同时,若两数为正数,绝对值小的小,否则绝对值大的小
return (sf1 != sf2) && (sign1 ^ (sf1 < sf2));
}
static bool sf32_real_le(sfloat32 sf1, sfloat32 sf2)
{
bool sign1 = SF32_SIGN(sf1);
bool sign2 = SF32_SIGN(sf2);
if (sign1 != sign2)
// 与之前不同,两数可以为0
return sign1 && (((sf1 | sf2) << 1) == 0);
// 与之前不同,相等或小于
return (sf1 == sf2) || (sign1 ^ (sf1 < sf2));
}
bool sf32_lt(sfloat32 sf1, sfloat32 sf2)
{
if (sf32_isnan(sf1) || sf32_isnan(sf2))
return false;
return sf32_real_lt(sf1, sf2);
}
bool sf32_le(sfloat32 sf1, sfloat32 sf2)
{
if (sf32_isnan(sf1) || sf32_isnan(sf2))
return false;
return sf32_real_le(sf1, sf2);
}
bool sf32_gt(sfloat32 sf1, sfloat32 sf2)
{
if (sf32_isnan(sf1) || sf32_isnan(sf2))
return false;
return !sf32_real_le(sf1, sf2);
}
bool sf32_ge(sfloat32 sf1, sfloat32 sf2)
{
if (sf32_isnan(sf1) || sf32_isnan(sf2))
return false;
return !sf32_real_lt(sf1, sf2);
}
在sf32_xx函数中,执行NaN的判断,而真正执行的sf32_real_xx函数里,就是纯粹的大小比较了。由于符号位的存在,实际大小比较需要针对同号和异号进行单独的判断,同时正负0是一样的。注意到这些细节这块代码也就不难理解了。
在正式讲四则运算的代码之前,先讨论一下舍入的问题。因为想要获得较为精确的结果,必须妥善处理舍入的问题,以免出现精度损失导致误差变大。IEEE规定二进制浮点运算有4种舍入模式,其中默认的也是最常用的是向最近偶数舍入,同时由于向0舍入较为容易实现,所以本文也会涉及。至于上舍入和下舍入,本文不作讨论。
本文将向最近偶数舍入模式简称为舍入模式,将向0舍入模式简称为截断模式。
比如在加减法的时候,需要做对齐指令的操作,此时小的那个数的尾数会右移,这样必然导致部分尾数的丢弃,此时我们需要保留一部分被移出的位,以确保运算的精度,同时在最后的包装阶段统一进行舍入操作。
按照一些书籍上说的,需要引入保护位(Guard bit),舍入位(Round bit)和粘贴位(Sticky bit),合称为GRS位,以确保舍入过程可以保证一定的精度。
保护位其实就是右移后剩下的最后一位,舍入位则是被移出去的最左边一位,粘贴位则取决于其余被移出位的逻辑或关系(也可以理解为是否不为0)。
举个例子,假如有一个6位的数11 0100(其实就是52的二进制),我们将其右移5位,那么剩下的就是00 0001,因此其保护位就是最后的1;被移出去的是1 0100,其粘贴位就是被移出去的最左边位,也是1;其余4位0100的逻辑或是1,或者说移出去的0100不为0,所以粘贴位就是1。如果我们将这个6位的数右边补0扩展到8位,即1101 00 00,那么将其右移5位后的结果就是0000 01 11,多出来的RS位可以参与运算,从而保留了一定的精度。
实现右移保留粘贴位的具体代码如下:
// 对于32位数,右移保留粘贴位
static uint32_t shift_right_sticky32(uint32_t sig, uint16_t n)
{
uint32_t res = 0;
if (n < 31) { // 范围判断
res = sig >> n;
if (sig << (32 - n)) // 判断粘贴位
res = res | 1;
}
else {
if (sig) // 如果右移超过31位,只需判断是否存在粘贴位
res = 1;
}
return res;
}
// 对于64位数,右移保留粘贴位
static uint64_t shift_right_sticky64(uint64_t sig, uint16_t n)
{
uint64_t res = 0;
if (n < 63) {
res = sig >> n;
if (sig << (64 - n))
res = res | 1;
}
else {
if (sig)
res = 1;
}
return res;
}
上述两个函数分别针对32位和64位,用来实现右移过程中粘贴位的保留。现在看这个可能觉得莫名其妙,不过后面就懂了。
这个方法相对比较复杂,因为要减少舍入误差,所以所有运算必须要使用GRS位来进行精度控制。
理论上来说,只需额外保持舍入位和粘贴位就可以实现舍入,但是实际上为了保证精度,我们通常也会在最后的舍入阶段额外保留完整的GRS位。
以十进制为例,通常我们的四舍五入是遇到了0.5就会+1,但是实际上这样会导致一个问题:1-4舍,5-9入,导致舍入发生的概率并不相同。因此有这样一种舍入,叫四舍六入五成双。即1-4舍,6-9入,而5看左边一位是不是偶数,是偶数则舍,否则入。比如说1.5舍入后就是2,但是2.5舍入后也是2,也就是遇到5的情况下使得左边一位是偶数。
那么对于二进制来说,也就是要保证最低位是0,确保结果是偶数。比如1.1应该舍入为10.0,10.1也是10.0。
对于保留了GRS位的情况,则GRS=100需要判断奇偶,其他情况就简单舍入。
所以舍入代码如下:
// 对于32位数,进行向偶数舍入操作,低3位分别为GRS位
static uint32_t round_even_grs_32(uint32_t sig)
{
uint8_t grs = sig & 7;
sig = sig >> 3;
if ((grs > 4) || ((grs == 4) && (sig & 1)))
sig++;
return sig;
}
由于舍入通常是最后一步,所以在舍入后就可以顺便将GRS位丢弃了。
不过可能会存在舍入后进位的情况,也就是所有位都是1的时候,+1就会导致溢出。
这个是最简单的了,直接放弃多余的位,右移就行了。但是在减法对齐指数的时候,不能直接丢弃,而加法对齐却是可以丢弃,所以看似简单其实也有一些玄机。
还有一个,就是加法和乘法结果如果出现上溢,在此模式下结果应该设置为浮点数可以表示的最大值,而不是正负无穷,这点需要注意。
说实话,加减法应该是最难的,相对来说加法稍简单一些。
与定点数加减法不同,由于浮点数不是按照补码存储的,所以不能简单将加减法结合在一起,而是要分开计算。
对于加法来说,有以下4种情况:
对于情况(1)和情况(4)来说,它们的结果的符号是确定的,所以可以直接加法。而情况(2)和情况(3)则实际上是减法。
对于减法来说,则是如下4种情况:
对于情况(6)和情况(7),实际上结果的符号是确定的,所以也是加法就可以搞定。情况(5)和情况(8)才是真正的减法。
因此我们可以把上述的1、4、6、7这4种情况按照加法处理,符号则是左操作数的符号;把2、3、5、8这4种情况按照减法处理,符号则需要进一步比大小来判断。
可以用下面的代码来进一步包装实际的加减法:
sfloat32 sf32_add(sfloat32 sf1, sfloat32 sf2)
{
if (SF32_SIGN(sf1) == SF32_SIGN(sf2))
return real_sf32_add(sf1, sf2);
return real_sf32_sub(sf1, sf2);
}
sfloat32 sf32_sub(sfloat32 sf1, sfloat32 sf2)
{
if (SF32_SIGN(sf1) == SF32_SIGN(sf2))
return real_sf32_sub(sf1, sf2);
return real_sf32_add(sf1, sf2);
}
即对于加法操作,同号才是真正的加法,否则实际是减法;对于减法操作,同号才是真正的减法,否则实际是加法。
加法的一般流程是:
先把完整的代码贴出来,后面详细说明具体的部分:
static sfloat32 real_sf32_add(sfloat32 sf1, sfloat32 sf2)
{
int exp1 = SF32_EXP(sf1);
int exp2 = SF32_EXP(sf2);
uint32_t sig1 = SF32_SIG(sf1);
uint32_t sig2 = SF32_SIG(sf2);
bool sign = SF32_SIGN(sf1);
int expdiff = exp1 - exp2;
if (expdiff == 0) { // 两数指数相同
if (exp1 == 0) // 忽略非规格化数
return ((uint32_t)sign << 31);
if (exp1 == 0xff) // 判断无穷或NaN
return sf32_nan_inf(sign, sig1 | sig2);
}
if (exp1 == 0xff)
return sf32_nan_inf(sign, sig1);
if (exp2 == 0xff)
return sf32_nan_inf(sign, sig2);
if (exp1 == 0)
return (sf2 & 0x7fffffff) | ((uint32_t)sign << 31);
if (exp2 == 0)
return (sf1 & 0x7fffffff) | ((uint32_t)sign << 31);
sig1 |= 0x800000;
sig2 |= 0x800000;
#ifndef ROUND_TRUNC
sig1 = sig1 << 2;
sig2 = sig2 << 2;
#endif
if (expdiff < 0) {
expdiff = -expdiff;
#ifdef ROUND_TRUNC
if (expdiff > 24)
sig1 = 0;
else
sig1 = sig1 >> expdiff;
#else
sig1 = shift_right_sticky32(sig1, expdiff);
#endif
exp1 = exp2;
}
else if (expdiff > 0) {
#ifdef ROUND_TRUNC
if (expdiff > 24)
sig2 = 0;
else
sig2 = sig2 >> expdiff;
#else
sig2 = shift_right_sticky32(sig2, expdiff);
#endif
}
uint32_t sig = sig1 + sig2;
#ifdef ROUND_TRUNC
if (sig > 0xffffff) {
sig = sig >> 1;
exp1++;
}
#else
if (sig < 0x4000000)
sig = sig << 1;
else
exp1++;
sig = round_even_grs_32(sig);
if (sig > 0xffffff)
exp1++;
#endif
if (exp1 > 254)
#ifdef ROUND_TRUNC
return (SF32_MAX | ((uint32_t)sign << 31));
#else
return (SF32_INF | ((uint32_t)sign << 31));
#endif
return sf32_pack(sign, exp1, sig);
}
这里引入了函数sf32_nan_inf,其具体如下,主要是用来区分无穷和NaN:
static inline sfloat32 sf32_nan_inf(bool sign, uint32_t sig)
{
if (sig)
return SF32_NAN;
return (SF32_INF | ((uint32_t)sign << 31));
}
根据上述流程,我分块讲解一下real_sf32_add()的代码:
int exp1 = SF32_EXP(sf1);
int exp2 = SF32_EXP(sf2);
uint32_t sig1 = SF32_SIG(sf1);
uint32_t sig2 = SF32_SIG(sf2);
bool sign = SF32_SIGN(sf1);
int expdiff = exp1 - exp2;
if (expdiff == 0) { // 两数指数相同
if (exp1 == 0) // 忽略非规格化数
return ((uint32_t)sign << 31);
if (exp1 == 0xff) // 判断无穷或NaN
return sf32_nan_inf(sign, sig1 | sig2);
}
if (exp1 == 0xff)
return sf32_nan_inf(sign, sig1);
if (exp2 == 0xff)
return sf32_nan_inf(sign, sig2);
if (exp1 == 0)
return (sf2 & 0x7fffffff) | ((uint32_t)sign << 31);
if (exp2 == 0)
return (sf1 & 0x7fffffff) | ((uint32_t)sign << 31);
上述的return sf32_nan_inf(sign, sig1 | sig2);这句对两数的有效数求算术或,这样可以判断两数是否存在NaN。
sig1 |= 0x800000;
sig2 |= 0x800000;
#ifndef ROUND_TRUNC
sig1 = sig1 << 2;
sig2 = sig2 << 2;
#endif
if (expdiff < 0) {
expdiff = -expdiff;
#ifdef ROUND_TRUNC
if (expdiff > 24)
sig1 = 0;
else
sig1 = sig1 >> expdiff;
#else
sig1 = shift_right_sticky32(sig1, expdiff);
#endif
exp1 = exp2;
}
else if (expdiff > 0) {
#ifdef ROUND_TRUNC
if (expdiff > 24)
sig2 = 0;
else
sig2 = sig2 >> expdiff;
#else
sig2 = shift_right_sticky32(sig2, expdiff);
#endif
}
这里需要注意一个细节,就是截断模式对齐如果指数差大于了24,实际上并不需要右移,直接置0即可。而对于舍入模式则需要保留粘贴位。
uint32_t sig = sig1 + sig2;
#ifdef ROUND_TRUNC
if (sig > 0xffffff) {
sig = sig >> 1;
exp1++;
}
#else
if (sig < 0x4000000)
sig = sig << 1;
else
exp1++;
sig = round_even_grs_32(sig);
if (sig > 0xffffff)
exp1++;
#endif
对于截断模式,只需判断结果是否大于了24位,然后移掉多余的位;而舍入模式,需要判断结果是否大于27位,在舍入之后可能存在溢出,此时只需指数+1即可,甚至不需要右移,因为已经都是0了(只有全为1的情况下才会溢出,那么结果必然全为0,而规格化是不需要保留1的)。
由于之前只保留的RS位,因此实际上应该是2个26位的数相加,所以结果有可能是26位或27位。对于26位的情况,需要将其扩展到27位,而在舍入时会舍弃低3位,所以实际上结果是24位,这个过程中没有出现实际的溢出;但是对于27位的情况,实际出现了溢出,所以需要将指数+1。
在之前仅扩展RS位的目的就是这里,可以通过一句简单的if else将两种情况结合且不出现任何右移操作,这样在保证精度的同时还可以减少生成的代码体积。
if (exp1 > 254)
#ifdef ROUND_TRUNC
return (SF32_MAX | ((uint32_t)sign << 31));
#else
return (SF32_INF | ((uint32_t)sign << 31));
#endif
return sf32_pack(sign, exp1, sig);
这个没什么好说的,关于上溢结果前面已经提到了。
减法的流程和加法挺不一样的,具体体现在符号判断,大小判断等。
基本流程如下:
不过流程是这样说的,实际代码还是有点不一样,直接放出完整代码,具体看一下代码的注释:
static sfloat32 real_sf32_sub(sfloat32 sf1, sfloat32 sf2)
{
int exp1 = SF32_EXP(sf1);
int exp2 = SF32_EXP(sf2);
uint32_t sig1 = SF32_SIG(sf1);
uint32_t sig2 = SF32_SIG(sf2);
uint32_t sig;
bool sign = SF32_SIGN(sf1); // 先取被减数的符号
int expdiff = exp1 - exp2;
if (expdiff == 0) { // 指数相同
if (exp1 == 0xff) // 不需要判断是不是无穷,因为无穷减无穷也是NaN
return SF32_NAN;
if (sig1 == sig2) // 尾数相同
return 0;
if (sig1 > sig2) { // 大减小
sig = sig1 - sig2;
}
else {
sig = sig2 - sig1;
sign = !sign; // 由于被减数小于减数,需要翻转符号
}
sig = sig << 3; // 留出GRS位,虽然对这里没什么用
}
else if (expdiff < 0) { // 被减数小于减数
sign = !sign; // 翻转符号
if (exp2 == 0xff)
return sf32_nan_inf(sign, sig2);
if (exp1 == 0)
return (sf2 & 0x7fffffff) | ((uint32_t)sign << 31);
exp1 = exp2;
expdiff = -expdiff;
sig1 = (sig1 | 0x800000) << 3; // 补上24位的1,留出GRS位
sig1 = shift_right_sticky32(sig1, expdiff); // 保留粘贴位,很重要
sig = ((sig2 | 0x800000) << 3) - sig1; // 减数减去被减数
}
else {
if (exp1 == 0xff)
return sf32_nan_inf(sign, sig1);
if (exp2 == 0)
return (sf1 & 0x7fffffff) | ((uint32_t)sign << 31);
sig2 = (sig2 | 0x800000) << 3;
sig2 = shift_right_sticky32(sig2, expdiff);
sig = ((sig1 | 0x800000) << 3) - sig2; // 这里就正常减法
}
// 为了舍入和规格化,需要把结果补齐到27位
while (sig < 0x4000000) { // 可以优化为求前导0数量
sig = sig << 1;
exp1--;
}
#ifdef ROUND_TRUNC
sig = sig >> 3;
#else
sig = round_even_grs_32(sig);
if (sig > 0xffffff) // 和之前一样
exp1++;
#endif
if (exp1 < 1) // 下溢判断
return ((uint32_t)sign << 31);
return sf32_pack(sign, exp1, sig);
}
对于ARM的CPU来说,存在汇编指令clz,可以直接获取一个寄存器开头0的个数(即前导0);对于Intel的CPU来说,有bsr可以达到同样的效果。不过这里为了保证代码通用性没有使用,实际上需要时可以针对架构专门优化。
相比于加减法,乘除法就简单了不少,其中乘法最容易实现。
和加减法不同的是,乘除法的符号位非常容易判断,对两个数的符号求异或即可。
乘法流程较为简单:
特殊值情况相对复杂,这个涉及到NaN和无穷以及0的一系列操作。任何数乘NaN都是NaN,任何数乘无穷都是无穷(除了0,结果是NaN),0乘任何数都是0,代码实现中判断顺序很重要。
尾数都是24位的,取值范围是[0x800000~0xFFFFFF],相乘会产生47~48位的结果,对于大部分32位CPU来说,都有64位数乘法的指令,所以可以直接计算,并没有额外的开销,只是64位结果右移时会有很小的开销。
完整代码如下,具体看注释,理解了加减法很多东西就没难度了:
sfloat32 sf32_mul(sfloat32 sf1, sfloat32 sf2)
{
int exp1 = SF32_EXP(sf1);
int exp2 = SF32_EXP(sf2);
uint32_t sig1 = SF32_SIG(sf1);
uint32_t sig2 = SF32_SIG(sf2);
bool sign = SF32_SIGN(sf1) ^ SF32_SIGN(sf2);
if (exp1 == 0xff) { // NaN或无穷
if (sig1 || ((exp2 == 0xff) && sig2) || (exp2 == 0)) // NaN*a或a*NaN或Inf*0
return SF32_NAN;
return (SF32_INF | ((uint32_t)sign << 31));
}
if (exp2 == 0xff) {
if (sig2 || (exp1 == 0)) // a*NaN或0*Inf
return SF32_NAN;
return (SF32_INF | ((uint32_t)sign << 31));
}
if ((exp1 == 0) || (exp2 == 0)) // 存在0
return ((uint32_t)sign << 31);
uint32_t sig;
int exp = exp1 + exp2 - 127; // 要减去偏移量
sig1 |= 0x800000;
sig2 |= 0x800000;
#ifdef ROUND_TRUNC
sig = ((uint64_t)sig1 * sig2) >> 23; // 相乘移位
if (sig > 0xffffff) { // 结果必然是24位或25位
sig = sig >> 1;
exp++;
}
#else
sig = shift_right_sticky64((uint64_t)sig1 * sig2, 21); // 少移2位,保留RS
if (sig < 0x4000000) // 这块代码和之前加法的操作如出一辙啊
sig = sig << 1;
else
exp++;
sig = round_even_grs_32(sig);
if (sig > 0xffffff)
exp++;
#endif
if (exp < 1)
return ((uint32_t)sign << 31);
if (exp > 254)
#ifdef ROUND_TRUNC // 和加法一样
return (SF32_MAX | ((uint32_t)sign << 31));
#else
return (SF32_INF | ((uint32_t)sign << 31));
#endif
return sf32_pack(sign, exp, sig);
}
除法比乘法稍微复杂一些。
其基本流程和乘法很像:
特殊值只要搞清楚0作为被除数和除数的区别,以及0与无穷的一些关系,就不赘述了。
唯一稍复杂的是尾数的相除,由于尾数是定点小数,不能直接相除,否则会引入极大的误差,所以需要将被除数扩大再使用定点数的除法,因为24位的定点数想要不损失精度,至少需要扩展到48位,同时由于舍入问题,还需要获取余数。
和乘法不同的是,不是所有的32位CPU都支持64位被除数去除以一个32位的除数。对于英特尔x86来说,大家都很熟悉div指令,如果操作数是32位寄存器,那么会将eax作为低位,edx作为高位去除以这个寄存器的值,将商存储在eax,余数存储在edx。而对于其他一些32位CPU来说,不一定支持这样的指令。
所以需要自行实现一个64位整数的除法,而且为了使用这个除法操作,需要确保两数相除的商要在32位。这实际上不难实现,请看如下代码:
// 64位无符号整数除以32位无符号整数,返回商,余数可选
// 确保结果不超过32位宽度
static inline uint32_t div_64_32_32_32(uint64_t n, uint32_t base, uint32_t *r)
{
uint64_t rem = n;
uint32_t quo = 0;
for (int i = 31; i >= 0; i--) {
b = (uint64_t)base << i;
if (rem > b) {
rem -= b;
quo |= (uint32_t)(1 << i);
}
}
if (r)
*r = rem;
return quo;
}
其实原理很简单啊,很容易想到的,就是移位,依次比大小,减得下就减,然后给结果置位。
有了这个以后呢,就可以实现除法操作了:
sfloat32 sf32_div(sfloat32 sf1, sfloat32 sf2)
{
int exp1 = SF32_EXP(sf1);
int exp2 = SF32_EXP(sf2);
uint32_t sig1 = SF32_SIG(sf1);
uint32_t sig2 = SF32_SIG(sf2);
bool sign = SF32_SIGN(sf1) ^ SF32_SIGN(sf2);
if (exp1 == 0xff) {
if (sig1 || (exp2 == 0xff)) // NaN/a或Inf/NaN或Inf/Inf
return SF32_NAN;
return (SF32_INF | ((uint32_t)sign << 31));
}
if (exp2 == 0xff) { // a/NaN
if (sig2)
return SF32_NAN;
return ((uint32_t)sign << 31);
}
if (exp1 == 0) {
if (exp2 == 0) // 0/0
return SF32_NAN;
return ((uint32_t)sign << 31); // 0/a
}
if (exp2 == 0) // a/0
return (SF32_INF | ((uint32_t)sign << 31));
int exp = exp1 - exp2 + 127; // 指数相减
sig1 = sig1 | 0x800000;
sig2 = sig2 | 0x800000;
uint64_t sig64;
if (sig1 < sig2) { // 比较大小,这是为了确保商一定是32位的
exp--;
sig64 = (uint64_t)sig1 << 32;
}
else {
sig64 = (uint64_t)sig1 << 31;
}
uint32_t sig;
#ifdef ROUND_TRUNC
sig = div_64_32_32_32(sig64, sig2, NULL) >> 8; // 忽略余数,直接右移
#else
uint32_t rem;
sig = div_64_32_32_32(sig64, sig2, &rem) >> 5; // 右移5位,留出GRS
if (rem) // 余数存在最低位置1,相当于粘贴位
sig |= 1;
sig = round_even_grs_32(sig); // 舍入处理
#endif
if (exp < 1)
return ((uint32_t)sign << 31);
if (exp > 254)
return (SF32_INF | ((uint32_t)sign << 31));
return sf32_pack(sign, exp, sig);
}
这个比较常用,也比较简单。
int32_t sf32_to_i32(sfloat32 sf)
{
int exp = SF32_EXP(sf) - 127;
if (exp < 0) // 指数过小返回0
return 0;
bool sign = SF32_SIGN(sf);
if (exp > 30) { // 指数过大则返回整数可以表示的最大值
if (sign)
return (int32_t)0x80000000;
else
return 0x7fffffff;
}
uint32_t sig = SF32_SIG(sf) | 0x800000;
int shift = 23 - exp;
int32_t res;
if (shift > 0)
res = sig >> shift;
else
res = sig << (-shift);
if (sign)
res = -res;
return res;
}
sfloat32 sf32_from_i32(int32_t i)
{
if (i == 0)
return 0;
bool sign = i >> 31;
if (sign)
i = -i;
int exp = 127 + 23;
while (i >= 0x1000000) { // 大了就右移,指数+1
i = i >> 1;
exp++;
}
while (i < 0x800000) { // 小了就左移,指数-1
i = i << 1;
exp--;
}
return sf32_pack(sign, exp, i);
}
对于64位整数同理,只需要适当修改一下代码即可。
有些浮点数的细节是很难完全说清楚的,这个需要自己实践一下才知道,比如我就是和CPU自带的浮点单元的结果进行比较判断是不是没有问题,我甚至专门写了个随机数的压力测试,基本上能扛住几秒钟就说明正常的运算就没有问题了。如果出现问题,就需要大量的调试操作了,因为这些运算细节很少有人说,所以只能一点一点摸索出来。
网上好多资料都是和计组有关的,极少数和硬件设计Verilog有关系,基本上就没有软件浮点数的。所以具体的参照非常少,也导致写起来非常缓慢。
而且IEEE-754 2008规定的基本运算平方根我也暂时没有高效率的实现,所以就不放出来献丑了,目前用的是卡马克平方根倒数的方法,直接二进制操作的方法我还是不清楚,毕竟不是学硬件的,就算学硬件的现在有那么多IP可以调谁还写这些是吧。
照着我这个思路,半精度浮点,双精度浮点应该都不难实现,实际上难的是怎么优化得效率更高一些,比如双精度浮点必然涉及更大的数的乘除法,这些如何优化是个问题。