1位符号位+5位指数位+10位尾数位,共16位,内存占2个字节
具体fp16表示法可以参照:机器学习-fp16表示
运算步骤
- 检查操作数中是否有0、Inf、NaN
NaN + a = Nan;
0 + a = a;
Inf + Inf = Inf;
Inf - Inf = NaN;
Inf + a = Inf;
-Inf + a = -Inf - 对阶
若两操作数阶码不相等,则需要进行对阶操作,使阶码变为一样,由于尾数右移比左移损失的精度小,因此会进行低阶向高阶对齐 - 对尾数进行加减并判断是否溢出
符号不同则尾数大的减尾数小的,符号相同则相加,此处判断是否产生进位。 - 规格化
如果产生进位,则阶码加1,尾数右移1位;如果没有进位,且尾数最高位与符号位数值相同,则阶码减1,尾数左移1位,直到尾数最高位为1为止 - Round to nearest even舍入处理
舍入到最接近且可以表示的值,当存在两个数一样接近时,取偶数值。(如2.4舍入为2,2.6舍入为3;2.5舍入为2,1.5舍入为2。)
当存在两个数一样接近时取偶数值是因为由于其他舍入方式均令结果单方向偏移,导致在运算时出现较大的统计偏差。而采用这种偏移则50%的机会偏移两端方向,从而减少偏差。 - 最终结果溢出判断
由于规格化可能导致阶码发生溢出,如果没有溢出,则直接返回结果;如果发生向上溢出,则返回Inf;如果发生向下溢出,则返回0
//判断是否为无效数:阶码全1,尾数非全0,则为无效数
int isNan(int data)
{
int exp = ((data & 0x7fff) >> 10) & 0x1f;
int mant = data & 0x3ff;
return ((exp == 0x1f) && (mant != 0)) ? 1 : 0;
}
//判断是否为正负无穷:阶码全1,尾数全0,则为无穷大,符号位为0,则为正无穷,符号位为1,则为负无穷
int isInf(int data)
{
int exp = ((data & 0x7fff) >> 10) & 0x1f;
int mant = data & 0x3ff;
return ((exp == 0x1f) && (mant == 0)) ? 1 : 0;
}
//判断是否为0:阶码全0,尾数全0,则为0,符号位为0,则为正零,符号位为1,则为负零
int isZero(int data)
{
int exp = ((data & 0x7fff) >> 10) & 0x1f;
int mant = data & 0x3ff;
return ((exp == 0) && (mant == 0)) ? 1 : 0;
}
int* fp16_Add(int* data1, int* data2)
{
unsigned int mant1, mant2, mantNew; //data1尾数,data2尾数,相加之后的尾数
unsigned int mantToExp; //尾数累加有进位
int sign1, sign2, signNew; //data1符号位,data2符号位,累加和符号位
int exp1, exp2, expNew; //data1阶码,data2阶码,累加和阶码
int tenBit = 0, elevenBit = 0, lastBit = 0;
int fp16Res;
sign1 = (data1 >> 15) & 0x01;
sign2 = (data2 >> 15) & 0x01;
exp1 = ((data1 & 0x7fff) >> 10) & 0x1f;
exp2 = ((data2 & 0x7fff) >> 10) & 0x1f;
mant1 = data1 & 0x3ff;
mant2 = data2 & 0x3ff;
//1. 检查操作数中是否有0、Inf、NaN
if(isNan(data1) || isNan(data2))
return 0x7c01; //NAN和任何数相加为NAN
if(isInf(data1) && isInf(data2) && (sign1 ^ sign2))
return 0x7c01; //Inf + -Inf = NAN
else if(isInf(data1))
return data1; //Inf + x = Inf
else if(isInf(data2))
return data2; //-Inf + x = -Inf
else if(isZero(data1))
return data2; //0 + x = x
else if(isZero(data2))
return data1;
//或0x400是加上前导数1,左移15位是为了,低阶向高阶对阶时,尾数会向右移(小数点会左移),这是为了保护有效位不丢失,高低指数若相差15位可忽略不计
mant1 = (mant1 | 0x400) << 15;
mant2 = (mant2 | 0x400) << 15;
//2.进行对阶操作,确定新exp,后续只需要进行尾数的累加
if(exp1 > exp2)
{
expNew = exp1;
mant2 >>= (exp1 - exp2); //data2阶码升高,尾数右移
exp2 = exp1;
}
else
{
expNew = exp2;
mant1 >>= (exp2 - exp1);
exp1 = exp2;
}
//3.尾数累加
if(sign1 ^ sign2) //一正一负
{
if(mant1 > mant2)
{
mantNew = mant1 - mant2;
signNew = sign1;
}
else
{
mantNew = mant2 - mant1;
signNew = sign2;
}
}
else //同正或同负
{
mantNew = mant1 + mant2;
signNew = sign1;
}
//4.判断有没有产生进位,并进行规格化
mantToExp = (mantNew >> 25) & 0x03; //尾数本身10位,添加了最高位(或0x40)最开始左移15位,所以右移25位,最后取到最高位
if(mantToExp & 0x02) //有进位
{
expNew++; //阶码+1
lastBit= mantNew & 0x01;
mantNew >>= 1; //尾数右移
}
else if(mantToExp == 0) //没有进位
{
if(mantNew == 0) //尾数全为0
{
return 0;
}
else
{
for(int i = 1;i <= 25; i++)//规格化
{
expNew--;
mantNew <<= 1;
if((mantNew >> 25) & 0x01)
break;
}
}
}
//5.Round to nearest even
tenBit = (mantNew >> 15) & 0x01; //第10位尾数是否为1
elevenBit= (mantNew >> 14) & 0x01; //第11位尾数是否为1
lastBit= (mantNew & ((1 << 14) - 1)) || lastBit; //后15是否存在1
mantNew >>= 15;
if(elevenBit&& (tenBit || lastBit)){
mantNew++; //舍入
}
if (mantNew & 0x800){ //舍入之后产生进位
expNew++;
mantNew >>= 1;
}
//6.溢出判断
if(expNew >= 31){ //上溢出
if(signNew == 1){
return 0xfc00; //返回负无穷
}
else{
return 0x7c00; //返回正无穷
}
}
if(expNew <= 0) //下溢出
{
return 0;
}
fp16Res = (((signNew & 0x01) << 15) | ((expNew & 0x1f) << 10) | (mantNew & 0x3ff));
return fp16Res;
}
/*
输入
data1:254 (0x5bf0, 0101 1011 1111 0000, 1.111111 * 2^(22-15));
data2:7.6836 (0x47af, 0100 0111 1010 1111, 1.11 1010 1111 * 2^(17-15));
输出:
fp16Res:261.75(0x5c17, 0101 1100 0001 0111, 1.00 0001 0111 * 2^(23-15));
7.6836:浮点数无法精确表示其数值范围内的所有数值,只能精确表示可用科学计数法1.m * 2^exp, 表示的数值而已,如0.5的科学计数法是2^-(1),则可被精确存储;而0.2则无法被精确存储)