半精度浮點數實驗

半精度浮點數實驗

來自專欄蘭灣

IEEE-754規定的單精度浮點數是4個位元組32位,包括1位符號、8位指數和23位尾數。它能表示的動態範圍是 2^{-126}sim2^{127} ,也就是 10^{-38} sim 10^{38} ;精度是 
m{lg} 2^{24} ,大約7個十進位有效數字。(另有其他不同規格的浮點數,不過估計現在極少見了。)

有時需要存儲或傳輸大量對精度要求不高、但動態範圍相對較大的數據,2位元組的整型最大只能到32767(有符號)或65535(無符號),範圍不夠,4位元組的單精度浮點數存儲和傳輸效率又偏低,怎麼辦呢?3個位元組在大部分場合都得考慮對齊訪問,更麻煩,所以要減就減到2位元組。

NVidia在2002年提出了半精度浮點數,只使用2個位元組16位,包括1位符號、5位指數和10位尾數,動態範圍是 2^{-30}sim 2^{31} 也就是 10^{-9}sim 10^9 ,精度是 
m lg2^{11} ,大約3個十進位有效數字。NVidia的方案已經被IEEE-754採納。Google的TensorFlow則比較簡單粗暴,把單精度的後16位砍掉,也就是1位符號、8位指數和7位尾數。動態範圍和單精度相同,精度只有 
m lg 2^8 ,2個有效數字。

這兩種半精度浮點數的實際使用效果如何呢?可以寫個程序驗證一下。

typedef unsigned short half; // 先定義unsigned short為half

按NVidia方案,把單精度浮點數轉成半精度,可以這麼做:

half Float2Half(float m){ unsigned long m2 = *(unsigned long*)(&m); // 強制把float轉為unsigned long // 截取後23位尾數,右移13位,剩餘10位;符號位直接右移16位; // 指數位麻煩一些,截取指數的8位先右移13位(左邊多出3位不管了) // 之前是0~255表示-127~128, 調整之後變成0~31表示-15~16 // 因此要減去127-15=112(在左移10位的位置). unsigned short t = ((m2 & 0x007fffff) >> 13) | ((m2 & 0x80000000) >> 16) | (((m2 & 0x7f800000) >> 13) - (112 << 10)); if(m2 & 0x1000) t++; // 四捨五入(尾數被截掉部分的最高位為1, 則尾數剩餘部分+1) half h = *(half*)(&t); // 強制轉為half return h ;}

從半精度轉回單精度比較好辦, 按格式取出符號位、指數和尾數,再按定義計算,結果保存為float即可。

float Half2Float(half n){ unsigned short frac = (n & 0x3ff) | 0x400; int exp = ((n & 0x7c00) >> 10) - 25; float m; if(frac == 0 && exp == 0x1f) m = INFINITY; else if (frac || exp) m = frac * pow(2, exp); else m = 0; return (n & 0x8000) ? -m : m;}

最後把實際數據從單精度轉成半精度,再轉回單精度,計算誤差:

for(float n = 4e-5; n < 6e4; n *= 1.001) { printf("%f, %f, %.4f
"
, n, Half2Float(Float2Half(n)), ((double)n - Half2Float(Float2Half(n))) / n * 100.0); }

實測最大誤差0.048%(也就是1/2048),平均絕對誤差0.018%,似乎還不錯。

Google TensorFlow的方案驗證起來就非常簡單了,砍掉後16位即可,四捨五入還是要的。

#include <stdio.h>int main(void){ for(float n = 1e-8; n < 1e8; n *= 1.001) { unsigned long k, l; k = *(unsigned long*)(&n); l = k & 0xffff0000; if(k & 0x8000) l += 0x10000; // 四捨五入 float m = *(float*)(&l); printf("%f, %f, %f
", n, m, (n - m) / n); } return 0;}

最大誤差0.39%(也就是1/256),平均絕對誤差0.14%。許多場合其實主要關心的只是數量級,用這個也不錯。

ps. 補充一下,AMD在顯卡上確實用過3個位元組/24位的浮點數,哦,那會兒還是ATI。微軟在Direct3D API里也提供了支持。現在不知道還有沒有了。

推薦閱讀:

TAG:編程語言 | C編程語言 | 浮點數 |