Yuens' blog
浮点数,如float
、double
在计算机中的表示为二进制,其分别占32位、64位(float为4字节,double为8字节,1字节=8位)。计算机来表示该数时,首先会将其表示为二进制形式的科学计数法。
虽然表示的形式是二进制的科学计数法,但存储是以三部分进行,以float
为例:
下图分别是float
和double
的二进制表示数的形式:
同理,120.5在内存中的存储格式如下:
补充:整数与小数的二进制转换
- 整数部分转二进制:短除法,最终从下向上得到结果;
- 小数部分转二进制:乘2取整,最终从上向下取得结果;
整数的表示只有两部分:
那么这么看来,float32
与int32
尾数部分分别为23(实际表示24,因为从小数点后计算)、31,精度上int
更高。但是float
因为有8位的指数部分,其表示的范围比整数大。
那么知道了整数和浮点在计算机中的表示后,那它们是怎么存储的呢?下面先讲两个来自百度百科的概念。
一般情况,我们的计算机都是小端模式。
下面以unsigned int value = 0x12345678
为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]
来表示value
。
高地址
---------------
buf[3] (0x78) -- 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位
---------------
低地址
高地址
---------------
buf[3] (0x12) -- 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位
--------------
低地址
内存地址 | 小端模式存放内容 | 大端模式存放内容 |
---|---|---|
0x4000 | 0x78 | 0x12 |
0x4001 | 0x56 | 0x34 |
0x4002 | 0x34 | 0x56 |
0x4003 | 0x12 | 0x78 |
目的:将32bit的float
浮点数组转为指定quant_data_bit_len
位数表示的定点数组,如将32量化为16位,quant_data_bit_len=16
。但实际转后的定点仍以float
表示(因为该硬件平台没有16bit的类型,因而这里QDTYPE
仍为float
),这里只是做软件层面的截断。
整体计算流程如下:
with_sign
。遍历数组中每个元素,判断是否存在负数。若有则with_sign=1
,没有负数则with_sign=0
;n * 2^n
后,计算其n
是多少。该过程遍历所有元素,得到数组中元素绝对值的最大值,并通过对数计算,用2^n
近似原浮点数,得到该上界的n
;quant_keep_fraction_bit_len
。用我们设定的量化策略的位数quant_data_bit_len
,减去符号位占用(若有),再减去指数位的占用,最后得到量化可用来表示尾数的尾数位个数quant_keep_fraction_bit_len
;quant_keep_fraction_bit_len
位,即对尾数左移量化能保留的尾数个数;int
强制转换,强制截断去掉小数部分得到integer_faction
;integer_fraction
右移回来,右移量化保留的尾数个数,恢复浮点表示(此时也完成了指定位数quant_data_bit_len
的量化操作);下面是该过程实现,入口函数是quant_tensor
(具体项目见ysh329/teapot):
int has_sign_bit(const DTYPE v)
{
DTYPE eps = 1e-5; // consider more high precision for DTYPE, such as double
int with_sign = -1;
if(v > eps)
with_sign = 0;
else
with_sign = (v <= -eps) ? 1 : -1;
return with_sign;
}
void set_tensor_quant_scheme(tensor_t *t, const int bit_len)
{
assert(t && bit_len);
DTYPE *data = t->data;
int with_sign = 0;
// judge whether sign bit exists
for(int eidx = 0; eidx < t->len; ++eidx)
{
DTYPE e = data[eidx];
if(has_sign_bit(e) == 1)
break;
}
t->with_sign = with_sign;
t->quant_data_bit_len = bit_len;
return;
}
int calc_exponent_high_bit(tensor_t *t)
{
assert(t && t->len > 0);
DTYPE *data = t->data;
DTYPE abs_max = fabs(*data);
for(int eidx = 1; eidx < t->len; ++eidx)
abs_max = (fabs(data[eidx]) > abs_max) ? fabs(data[eidx]) : abs_max;
int exponent_high_bit = (abs_max == 0) ? 0 : floor(log(abs_max) / log(2));
return exponent_high_bit;
}
tensor_t* quant_tensor(tensor_t *t, const int quant_data_bit_len)
{
assert(t && t->data_bit_len >= quant_data_bit_len);
set_tensor_quant_scheme(t, quant_data_bit_len);
// elem_bit_len_without_sign: 32(elem_bit_len=32, with_sign=0), 31(elem_bit_len=32, with_sign=1),
// 16(elem_bit_len=16, with_sign=0), 15(elem_bit_len=16, with_sign=1),
// 8(elem_bit_len= 8, with_sign=0), 7(elem_bit_len= 8, with_sign=1),
int elem_bit_len_without_sign = t->with_sign ? t->quant_data_bit_len-1 : t->quant_data_bit_len;
int exponent_high_bit = calc_exponent_high_bit(t);
int quant_keep_fraction_bit_len = exponent_high_bit - elem_bit_len_without_sign + 1;
DTYPE *data = t->data;
QDTYPE *qdata = t->quant_data;
for(int eidx = 0; eidx < t->len; ++eidx)
{
DTYPE e = data[eidx];
int integer_fraction = (int)(e * pow(2, -quant_keep_fraction_bit_len)); // float2fixed: cutoff part of fraction by left move
qdata[eidx] = integer_fraction * pow(2, quant_keep_fraction_bit_len); // fixed2float: resume float by right move
}
return t;
}
看了上述代码,不难分析出我们表示16位的数,也是将其表示成三部分:首先检查是否有符号位,若有则剩下的15位用来表示指数位和尾数位,又因为上述代码中的指数位完全是根据该数值的指数位来定的,因此再去除指数位的个数后,剩下的全部用来表示尾数位。这种方式只是我们的一种模拟,实际硬件支持的方式还需看IEEE的定义。
gcc在16位浮点上,既有软件层面也有硬件层面的支持,具体见:Half-Precision Floating Point。16位浮点的标准参考IEEE 754-2008。