最近在研究CNN的硬件实现,其中涉及到特征图像、权重和偏移文件的量化,需要研究浮点数、定点数之间的转化,如果权重浮点数转化为标准的定点数,则需要自己写定点数的乘法和加法运算,因为在C中,int,short等整形是用补码进行运算的,但是定点数(负数)和补码形式不一样,如果直接用加法,设计到负数运算时,出错。
一.浮点数
C语言中有3种浮点数,float型、double型和long double型,其中float型占4个字节,double型占8个字节,longdouble型长度要大于等于double型,本文档将以float型为例进行介绍,double型和long double型只是比float型位数长,原理都是一样的。
float型可以表示的范围是-3.402823466e38-3.402823466e38,而作为同为4个字节的定点数却只能表示-2147483648~2147483647的范围,使用同样的内存空间,浮点数却能比定点数表示大得多的范围,这是不是太神奇了?既然浮点数能表示这么大的范围,那么我们为何不使用浮点数来代替定点数呢?
先不说浮点数实现起来比较复杂,有些处理器还专门配置了硬件浮点运算单元用于浮点运算,主要原因是浮点数根本就无法取代定点数,因为精度问题。鱼和熊掌不可兼得,浮点数表示了非常大的范围,但它失去了非常准的精度。在说明精度问题前,我们先了解一下浮点数的格式。
ANSI/IEEEStd 754-1985标准
IEEE 754是最广泛使用的二进制浮点数算术标准,被许多CPU与浮点运算器所采用。IEEE754规定了多种表示浮点数值的方式,在本文档里只介绍32bits的float浮点类型。它被分为3个部分,分别是符号位S(sign bit)、指数偏差E(exponent bias)和尾数位F(fraction)。
单精度浮点数数据位宽共有 32 位,可以分为三个部分,其中符号位 S 只有 1 位,指数位E 为 8 位,尾数位 F 为 23 位,其代数形式为:
若一个浮点数为0x41040000,在计算机中存储的形式为0100 0001 0000 0100 0000 0000 0000 0000b,
符号位为:0
指数位为:1000 0010b = 130 130-127=3
尾数位为:000 0100 0000 0000 0000 0000
1.000 0100 0000 0000 0000 0000 = 1+(1/2^5) = 1.03125
1.03125*2^2 = 8.25
因此,该数据表示的浮点数为:8.25
二、定点数
参与数值运算的数为16位的整型数。但在许多情况下,数学运算过程中的数不一定都是整数。
应该说,运算芯片本身无法处理小数。关键就是由程序员来确定一个数的小数点处于16位中的哪一位。这就是数的定标。
{通过设定小数点在16位数中的不同位置,就可以表示不同大小和不同精度的小数}
数的定标有Q表示法和S表示法两种。下面列出了一个16位数的16种Q表示、S表示及它们所能表示的十进制数值范围:
Q表示 S表示 十进制数表示范围
Q15 S0.15 -1≤x≤0.9999695
Q14 S1.14 -2≤x≤1.9999390
Q13 S2.13 -4≤x≤3.9998779
Q12 S3.12 -8≤x≤7.9997559
Q11 S4.11 -16≤x≤15.9995117
Q10 S5.10 -32≤x≤31.9990234
Q9 S6.9 -64≤x≤63.9980469
Q8 S7.8 -128≤x≤127.9960938
Q7 S8.7 -256≤x≤255.9921875
Q6 S9.6 -512≤x≤511.9804375
Q5 S10.5 -1024≤x≤1023.96875
Q4 S11.4 -2048≤x≤2047.9375
Q3 S12.3 -4096≤x≤4095.875
Q2 S13.2 -8192≤x≤8191.75
Q1 S14.1 -16384≤x≤16383.5
Q0 S15.0 -32768≤x≤32767
2.1 定点表示示例:
同样一个16位数,若小数点设定的位置不同,它所表示的数也不同(首位为符号位):
16进制数2000 H= 二进制数0 010 0000 0000 0000 B= 十进制数8192, Q0表示法
16进制数 2000 H= 二进制数0 010 0000 0000 0000 B= 十进制数0.25 , Q15表示法
三、 浮点定点转换:
不同的Q所表示的数不仅范围不同,而且精度也不相同。
Q越大,数值范围越小,但精度越高;相反,Q越小,数值范围越大,但精度就越低。
E.g.
Q0 的数值范围是-32768到+32767,其精度为1;而Q15的数值范围为-1到0.9999695,精度为1/32768=0.00003051。
因此,对定点数而言,数值范围与精度是一对矛盾。
一个变量要想能够表示比较大的数值范围,必须以牺牲精度为代价;而想精度提高,则数的表示范围就相应地减小。
在实际的定点算法中,为了达到最佳的性能,必须充分考虑到这一点。
3.2 转换关系:
浮点数与定点数的转换关系可表示为:
浮点数(Fx)转换为定点数(Ix):Ix = (int)x* 2^Q
定点数(Ix)转换为浮点数(Fx):Fx= (float)Ix*2^(-Q)
3.3 转换示例:
浮点数 Fx = 0.5,定标 Q = 15,则定点数:
Ix = floor(0.5*32768) = 16384
反之,一个用 Q = 15 表示的定点数Ix = 16384,其浮点数为:
Fx = 16384 * 2^(-15) = 16384 / 32768 = 0.5
浮点数转换为定点数时,为了降低截尾误差,可以在取整前可以先加上0.5,视情况而定。
四、程序验证
#define _CRT_NONSTDC_NO_DEPRECATE
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
float value;
float *var = &value;
int value_int;
int *var_int = &value_int;
scanf("%f", var);
scanf("%d", var_int);
printf("%x\n", *((int*)var));//%x 16进制输出整数
printf("%x\n", *var_int);
return 0;
}
结果如下:
可见,浮点数在计算机中的保存、数据处理是按照浮点数标准进行的,int,short等整形数据类型是按照二进制补码来表示的。
浮点数转定点数的程序如下:
Qn为浮点数定标;
short float2fixed_fun(float fdata, int Qn){
short sdata;
int temp;
int integer = 2 << (Qn - 1);
if (fdata > 0){
temp = int((fdata * integer));
sdata = (temp == short(temp)) ? temp : (temp >> 31) ^ 0x7fff;
}
else if (fdata < 0){
fdata = -fdata;
temp = int((fdata * integer));
sdata = (temp == short(temp)) ? temp : (temp >> 31) ^ 0x7fff;
sdata = sdata ^ SIGN_BIT;
}
else
sdata = 0;
return sdata;
}
定点数转浮点数程序如下:
float fixed2float_fun(short sdata, int Qn){
int sign_flag = sdata & SIGN_BIT;//1->负数 0->正数
float fdata,temp;
short temp1;
int integer = 2 << (Qn - 1);
if(sign_flag == 0){ //该定点数为正
fdata = float(sdata)/integer;
}
else{
temp1 = sdata ^ SIGN_BIT;
//temp = float(sdata ^ SIGN_BIT);直接使用此语句,结果不正确,不知为何
temp = float(temp1);
fdata = temp / integer;
fdata = -fdata;
}
return fdata;
}
C语言小白,从研究浮点数定点数到写好程序用了四天,发现网上的介绍很多,但是开源的代码不多,就把自己写的开源吧,包括浮点定点转换,定点乘法、加法运算等。供参考。
github源码地址:https://github.com/alangaixiaoxiao/CNN-float-fixed-translation-
四、浮点定点转换(补码表示)
负数在计算机中是用补码的形式存储的,正数在计算机中是用原码的形式存储的。
正数求原码直接将十进制转二进制即可,负数的补码是在原码的基础上除符号位外其余位取反后+1。
之前是将数据用标准的定点数表示方法来进行表示和运算的,这样有一个问题,那就是运算的时候,还需要自己针对定点数的标准写乘法器和加法器,尤其是在硬件运算时,需要去优化。因此,也可以用补码的形式了表示负数定点数。转换代码如下:
//浮点数转定点数
short fixed_data = (short)(float_data*pow(2.0,Qn));
//定点数转浮点数
float float_data = (float)(fixed_data*pow(2.0,-Qn));