分类
tech

关于浮点数(2)half2float

闲来无事(真的吗)分析一下 half2float() 的实现(好吧其实是有人问,然后看到这个函数就觉得挺有意思的)

多翻翻 GLM 果然是一个非常涨姿势的活动。。。

Half 和 float 的规格

首先当然是看定义,half 和 float32 的不同位数的含义 IEEE-754 是这么规定的:

Difference Between Single-, Double-, Multi-, Mixed-Precision | NVIDIA Blog

区别就在于,指数 E 和尾数 M 所使用的位数不同

  • 对于指数 E,half 用 5bit 表示,float 用 8bit
  • 对于尾数 M,half 用 10bit,float 用 23bit

那么转换思路当然就是:分别对符号位 S、指数 E、尾数 M进行位移,并且注意要先给指数 E 进行合适的 offset,最后组合一下即可

0.5 as an example

首先需要用具体的例子代入,热身一下,看看 0.5 用 half 和 float 分别是怎么表示的:

首先 0.5 是十进制的表示,转换成二进制就是 0.1,再用科学计数法表示就是 1.0 * 2^(-1)

用 IEEE 754 标准来看,也就是:

符号位 s = 0,指数位 E = -1,尾数位 M = 0 (因为科学计数法第一位肯定是1,就不用表示了)

指数 E 的 offset

这里还有一点需要注意,由于指数 E 有可能为正或者为负(代表原数绝对值大于1或小于1),所以浮点数的指数表示正负数的位数各占一半,例如 half5bit 表示指数,即一共可以表示 2^5 = 32 的范围(0~31),于是它采用前半部分表示负数([0, 15],即 00000-01111),后部分表示正数([16, 31],即 10000-11111)。

因此,我们计算 half 的指数位 E 的时候,需要给它加上 15 作为 offset,同理,对于 8bit 表示指数的 float,offset 为 127。

所以,对于 0.5,其指数部分 E:

用 half 表示的时候,指数为 E = -1 = -1+15 = 14 = [0 1110]

用 float 表示的时候,指数为 E = -1 = -1+127 = 126 = [0111 1110]

用 half 表示 0.5

用 float 表示 0.5

所以,当我们谈论 half2float 的时候,对 0.5 来说,实际上发生的事情就是:

函数 half2float

half2float 只有一行:

把它拆开来分析一下就很简单了:

step1: 取符号位左移 16 位

((half & 0x8000) << 16)

0x8000 = [1000 0000 0000 0000‬]

这是一个只有最左侧(最高位)为 1 的数,任何数和它做与运算,都只会保留最高位

half & 0x8000 得到的结果,就是最高位(最左侧)的符号位,然后 << 16 就是向左移动 16 bit

(得到的就是 0 ,因为我们的目的就是只保留符号位0)

再把结果向左移动 16 位 (从 16bit 补齐为 32bit)得到

step2: 取指数部分+offset,再左移 13 位

((half & 0x7c00) + 0x1C000)

0x7c00 = [0111 1100 0000 0000]

这是一个只有第 2-6 位为 1 的数,通过和它做与运算,用 0x7c00 就可以把 half 的指数位(从左开始第 2 位到第 6 位 取出来)

取出了 half 的指数部分,再来就是给它加上 127-15 = 112 的 offset

二进制表示的 112 = [0111 0000],由于对于 half 来说,指数位是从第2-6位,也就是右侧有 10 个位数是用于表示尾数的,那么为了给 half 的指数位 + 112,也需要给它右侧补 10 个0,即

[0111 0000] => [01 1100 0000 0000 0000] => 十六进制的 0x1C000

用刚刚的结果,加上 0x1C000,就是

再把结果向左移动 13 位 (补齐为 32bit)即

step3: 取尾数部分左移 13 位

(half & 0x03FF) << 13

0x03ff = [0000 0011 1111 1111]

它标记为 1 的,就是 half 里用来表示尾数 M 的最后 10 bit

再把结果向左移动 13 位 (补齐为 32bit)即

step4: 最终把 3 次结果用或运算组合一下

实现了 half 2 float 的转换