闲来无事(真的吗)分析一下 half2float()
的实现(好吧其实是有人问,然后看到这个函数就觉得挺有意思的)
多翻翻 GLM 果然是一个非常涨姿势的活动。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// https://github.com/g-truc/glm/blob/master/glm/gtc/packing.inl GLM_FUNC_QUALIFIER glm::uint16 float2half(glm::uint32 f) { // 10 bits => EE EEEFFFFF // 11 bits => EEE EEFFFFFF // Half bits => SEEEEEFF FFFFFFFF // Float bits => SEEEEEEE EFFFFFFF FFFFFFFF FFFFFFFF // 0x00007c00 => 00000000 00000000 01111100 00000000 // 0x000003ff => 00000000 00000000 00000011 11111111 // 0x38000000 => 00111000 00000000 00000000 00000000 // 0x7f800000 => 01111111 10000000 00000000 00000000 // 0x00008000 => 00000000 00000000 10000000 00000000 return ((f >> 16) & 0x8000) | // sign ((((f & 0x7f800000) - 0x38000000) >> 13) & 0x7c00) | // exponential ((f >> 13) & 0x03ff); // Mantissa } GLM_FUNC_QUALIFIER glm::uint half2float(glm::uint h) { return ((h & 0x8000) << 16) | ((( h & 0x7c00) + 0x1C000) << 13) | ((h & 0x03FF) << 13); } |
Half 和 float 的规格
首先当然是看定义,half 和 float32 的不同位数的含义 IEEE-754 是这么规定的:
区别就在于,指数 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),所以浮点数的指数表示正负数的位数各占一半,例如 half
用 5bit
表示指数,即一共可以表示 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
只有一行:
1 |
((h & 0x8000) << 16) | ((( h & 0x7c00) + 0x1C000) << 13) | ((h & 0x03FF) << 13) |
把它拆开来分析一下就很简单了:
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 的转换