Peripateticism

Yuens' blog

View the Project on GitHub

header

计算机的浮点表示与浮点数转定点操作

浮点数,如floatdouble在计算机中的表示为二进制,其分别占32位、64位(float为4字节,double为8字节,1字节=8位)。计算机来表示该数时,首先会将其表示为二进制形式的科学计数法。

1. 计算机中浮点的表示

image

虽然表示的形式是二进制的科学计数法,但存储是以三部分进行,以float为例:

  1. 符号位(sign,占1位,不存在无符号浮点):0代表正,1代表为负;
  2. 指数位(exponent,占8位):用于存储科学计数法中的指数数据,并且采用移位存储(存时对算出的2^m量级中的m,存m加上127的结果进这8位;取指数时对当前的8位指数的结果减去127)。IEEE754 中规定,单精度浮点数的指数域是 8 个比特,因此它的指数偏移值为 2^(8−1)−1=127;64 位双精度浮点数的值数域是 11 个比特,它的指数偏移值为:2^(11−1)−1=1023。指数编码时需要加上一个偏移值,是因为指数可以是负数,而计算机存储浮点数的“指数部分”又是一个无符号的整数,因此,在 IEEE754 标准规定,exponent 必须减去一个偏移值而得到真实的“指数值”;
  3. 尾数位(fraction,占23位):尾数部分。用二进制的科学计数法表示二进制,其第一位都是1,因而尾数位直接从小数点后面的第一位开始表示。所以23bit的尾数部分,可以表示的精度却变成了24bit,道理就是在这里。

下图分别是floatdouble的二进制表示数的形式:

image

image

2. 浮点数的表示例子

同理,120.5在内存中的存储格式如下:

补充:整数与小数的二进制转换

  • 整数部分转二进制:短除法,最终从下向上得到结果;
  • 小数部分转二进制:乘2取整,最终从上向下取得结果;

3. 计算机中整数的表示

整数的表示只有两部分:

  1. 符号位:占1位,若无符号,则不计;
  2. 尾数位:剩下的全部用来做尾数,如int32;

那么这么看来,float32int32尾数部分分别为23(实际表示24,因为从小数点后计算)、31,精度上int更高。但是float因为有8位的指数部分,其表示的范围比整数大。

4. 存储方式与大小端模式

那么知道了整数和浮点在计算机中的表示后,那它们是怎么存储的呢?下面先讲两个来自百度百科的概念。

一般情况,我们的计算机都是小端模式。

5. 大小端例子

下面以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value

  1. Big-Endian: 低地址存放高位,如下:
    高地址
      ---------------
      buf[3] (0x78) -- 低位
      buf[2] (0x56)
      buf[1] (0x34)
      buf[0] (0x12) -- 高位
      ---------------
      低地址
    
  2. Little-Endian: 低地址存放低位,如下:
    高地址
      ---------------
      buf[3] (0x12) -- 高位
      buf[2] (0x34)
      buf[1] (0x56)
      buf[0] (0x78) -- 低位
      --------------
    低地址
    
内存地址 小端模式存放内容 大端模式存放内容
0x4000 0x78 0x12
0x4001 0x56 0x34
0x4002 0x34 0x56
0x4003 0x12 0x78

6. 浮点转定点

目的:将32bit的float浮点数组转为指定quant_data_bit_len位数表示的定点数组,如将32量化为16位,quant_data_bit_len=16。但实际转后的定点仍以float表示(因为该硬件平台没有16bit的类型,因而这里QDTYPE仍为float),这里只是做软件层面的截断。

整体计算流程如下:

  1. 判断是否存在符号位with_sign。遍历数组中每个元素,判断是否存在负数。若有则with_sign=1,没有负数则with_sign=0
  2. 计算指数位的高位。即对浮点元素表示为二进制的科学记数法n * 2^n后,计算其n是多少。该过程遍历所有元素,得到数组中元素绝对值的最大值,并通过对数计算,用2^n近似原浮点数,得到该上界的n
  3. 计算尾数位数quant_keep_fraction_bit_len。用我们设定的量化策略的位数quant_data_bit_len,减去符号位占用(若有),再减去指数位的占用,最后得到量化可用来表示尾数的尾数位个数quant_keep_fraction_bit_len
  4. 将要量化的元素逐个遍历:
    1. 对该元素左移quant_keep_fraction_bit_len位,即对尾数左移量化能保留的尾数个数
    2. 再通过int强制转换,强制截断去掉小数部分得到integer_faction
    3. 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。

参考