簡介
由于計(jì)算機(jī)存儲的規(guī)則所致,有些時(shí)候浮點(diǎn)數(shù)存入和讀取出來的值并不相等,同樣的數(shù)據(jù)用單精度(float)和用雙精度(double)存儲,獲取出來的值也會有差異。所以,當(dāng)我們開發(fā)對精度要求比較高的業(yè)務(wù)場景下,如果不了解,很可能出現(xiàn)直接的經(jīng)濟(jì)損失。針對這些問題,接下來用一篇文章來詳細(xì)講解這些問題和解決方案。
例子
public static void main(String[] args) {
double d1 = 0.1;
double d2 = 0.1f;
float d3 = 0.1f;
System.out.println("d1=" + d1);
System.out.println("d2=" + d2);
System.out.println("d3=" + d3);
}
輸出:
d1=0.1
d2=0.10000000149011612
d3=0.1
- 第一個(gè)問題
為什么d1和d2打印的結(jié)果不同,d2打印的值為什么不是0.1,那d2后面149011612又是怎么來的呢? - 第二個(gè)問題
都說浮點(diǎn)數(shù)存在精度問題,當(dāng)讀取的時(shí)候會和存入時(shí)的值不同,那為何d3打印出來就是0.1呢?難道d3在計(jì)算機(jī)里存的本來就是0.1?
如果想要解釋上面的問題,那么就需先了解浮點(diǎn)數(shù)在計(jì)算機(jī)中的存儲方式(遵循IEEE 754(IEEE Standard for Floating-Point Arithmetic)標(biāo)準(zhǔn))。
十進(jìn)制數(shù)轉(zhuǎn)換為二進(jìn)制數(shù)
- 整數(shù)轉(zhuǎn)二進(jìn)制
十進(jìn)制整數(shù)轉(zhuǎn)換為二進(jìn)制整數(shù)采用"除2取余,逆序排列"法。 - 十進(jìn)制小數(shù)轉(zhuǎn)換為二進(jìn)制小數(shù)
十進(jìn)制小數(shù)轉(zhuǎn)換成二進(jìn)制小數(shù)采用"乘2取整,順序排列"法。具體做法是:用2乘十進(jìn)制小數(shù),可以得到積,將積的整數(shù)部分取出,再用2乘余下的小數(shù) 部分,又得到一個(gè)積,再將積的整數(shù)部分取出,如此進(jìn)行,直到積中的小數(shù)部分為零,或者達(dá)到所要求的精度為止。 然后把取出的整數(shù)部分按順序排列起來,先取的整數(shù)作為二進(jìn)制小數(shù)的高位有效位,后取的整數(shù)作為低位有效位。
aa.png
浮點(diǎn)數(shù)存儲格式
-
IEEE 754規(guī)定,對于32位的浮點(diǎn)數(shù)(單精度float),最高的1位是符號位s,接著的8位是指數(shù)E,剩下的23位為有效數(shù)字M。
float.png -
對于64位的浮點(diǎn)數(shù)(雙精度double),最高的1位是符號位S,接著的11位是指數(shù)E,剩下的52位為有效數(shù)字M。
double.png
IEEE 754對有效數(shù)字M和指數(shù)E的一些特別規(guī)定。
- 1≤M<2,也就是說,M可以寫成1.xxxxxx的形式,其中xxxxxx表示小數(shù)部分。IEEE 754規(guī)定,在計(jì)算機(jī)內(nèi)部保存M時(shí),默認(rèn)這個(gè)數(shù)的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的時(shí)候,只保存01,等到讀取的時(shí)候,再把第一位的1加上去。這樣做的目的,是節(jié)省1位有效數(shù)字。以32位浮點(diǎn)數(shù)為例,留給M只有23位,將第一位的1舍去以后,等于可以保存24位有效數(shù)字。
至于指數(shù)E,情況就比較復(fù)雜。
-
首先,E為一個(gè)無符號整數(shù)(unsigned int)。這意味著,如果E為8位,它的取值范圍為0 ~ 255;如果E為11位,它的取值范圍為0 ~ 2047。但是,我們知道,科學(xué)計(jì)數(shù)法中的E是可以出現(xiàn)負(fù)數(shù)的,所以IEEE 754規(guī)定,E的真實(shí)值必須再減去一個(gè)中間數(shù),對于8位的E,這個(gè)中間數(shù)是127;對于11位的E,這個(gè)中間數(shù)是1023。
比如,2^10的E是10,所以保存成32位浮點(diǎn)數(shù)時(shí),必須保存成10+127=137,即10001001。 -
然后,指數(shù)E還可以再分成三種情況:
E不全為0或不全為1。這時(shí),浮點(diǎn)數(shù)就采用上面的規(guī)則表示,即指數(shù)E的計(jì)算值減去127(或1023),得到真實(shí)值,再將有效數(shù)字M前加上第一位的1。
E全為0。這時(shí),浮點(diǎn)數(shù)的指數(shù)E等于1-127(或者1-1023),有效數(shù)字M不再加上第一位的1,而是還原為0.xxxxxx的小數(shù)。這樣做是為了表示±0,以及接近于0的很小的數(shù)字。
E全為1。這時(shí),如果有效數(shù)字M全為0,表示±無窮大(正負(fù)取決于符號位s);如果有效數(shù)字M不全為0,表示這個(gè)數(shù)不是一個(gè)數(shù)(NaN)。
浮點(diǎn)數(shù)舍入規(guī)則
而 IEEE 754 就是采用向最近偶數(shù)舍入(round to nearest even)的規(guī)則。
- 向丟失精度最小的方向向上/向下舍入
- 如果向上/向下舍入丟失精度一樣,者向偶數(shù)舍入
比如0.1的二進(jìn)制是1.10011001100110011001100110011001100110011001100110011...
根據(jù)單精度有效位M長度時(shí)23:
1.10011001100110011001100110011001100110011001100110011...,顯然向上舍入丟失的精度更小,所以0.1單精度存儲為:10011001100110011001101而不是10011001100110011001100
舉例
這里以小數(shù)0.1以單精度和雙精度存儲位例進(jìn)行講解。首先把0.1轉(zhuǎn)換成二進(jìn)制的形式是(轉(zhuǎn)換網(wǎng)站)
000110011001100110011001100110011001100110011001100110011...
- 把
0.1以單精度存儲
根據(jù)上面的存儲規(guī)則和有效數(shù)M表示規(guī)則,需要把0.1表示的二進(jìn)制向右移動4位,相當(dāng)于乘以2^4,那么可以得出指數(shù)部分應(yīng)該就是 127 - 4 = 123,二進(jìn)制就是01111011,由于向右移動4位,那么上面二進(jìn)制變成1.10011001100110011001100110011001100110011001100110011...,由于M的第一位可以不存儲,二單精度的有效位是23,那么截取小數(shù)點(diǎn)后23位存儲下來,其它舍去,在根據(jù)浮點(diǎn)數(shù)舍入規(guī)則最終存入的有效位二進(jìn)制:10011001100110011001101,加上第一位符號位,那么在0.1在計(jì)算機(jī)存儲為:0 01111011 10011001100110011001101 符號位(S) 指數(shù)位(E) 有效位(M) - 把
0.1以雙精度存儲(64位)0 01111111011 1001100110011001100110011001100110011001100110011010 符號位(S) 指數(shù)位(E) 有效位(M)
二進(jìn)制小數(shù)轉(zhuǎn)十進(jìn)制
以上面0.1單精度存儲二進(jìn)制1. 10011001100110011001101進(jìn)行轉(zhuǎn)換:
2^0 + 2^-1 + 2^-4 + 2^-5 + 2^-8 + 2^-9 + 2^-12 + 2^-13 + 2^-16 + 2^-17 + 2^-20 + 2^-21 + 2^-23
上面加起來的值在乘以指數(shù)2^-4就得到0.1存儲在計(jì)算機(jī)的值了。
經(jīng)過計(jì)算上面最終的值是:0.10000000149011612...
浮點(diǎn)數(shù)的有效位數(shù)
單精度的尾數(shù)用23位存儲,加上預(yù)設(shè)的小數(shù)點(diǎn)前不做存儲的
1這一位,那么可以表示的最大數(shù):2^(23+1) = 16777216。因?yàn)?10^7 < 16777216 < 10^8,所以說單精度浮點(diǎn)數(shù)的有效位數(shù)是7位。雙精度的尾數(shù)用52位存儲,2^(52+1) = 9007199254740992,因?yàn)?0^16 < 9007199254740992 < 10^17,所以雙精度的有效位數(shù)是16位。
解釋上面的問題
float d1 = 0.1f輸出為什么是0.1
根據(jù)上面講解的點(diǎn)數(shù)的有效位數(shù),單精度最大保留8位有效數(shù)。所以截去多余的就變成0.10000000即為0.1,這里看上去好像沒有出現(xiàn)精度問題。double d1 = 0.1f輸出為什么是0.10000000149011612
首先,'0.1f'按照單精度進(jìn)行存儲,讀取出來的值賦值給雙精度d1,根據(jù)上面講解的點(diǎn)數(shù)的有效位數(shù),雙精度最大保留17位有效數(shù),截去多余部分:0.10000000149011612double d1 = 0.1輸出為什么是0.1而不是0.10000000149011612
這里0.1按照雙精度存儲,由于雙精度的尾數(shù)用52位存儲,精度更高,大家可以把52位二進(jìn)制加起來算一下,把最后得到的數(shù)保留17位有效數(shù),看是不是也是0.1,答案,是肯定的。這里有個(gè)簡單的方式打印
public static void main(String[] args) {
System.out.println("aa= " + new BigDecimal(0.1));
}
輸出
aa= 0.1000000000000000055511151231257827021181583404541015625
精度丟失
public static void main(String[] args) {
float d4 = 111111.01111111f;
System.out.println("d4=" + d4);
}
輸出
d4=111111.01
大家可以按照上面的思路轉(zhuǎn)換一下??梢砸姷茫褂酶↑c(diǎn)數(shù)時(shí),如果整數(shù)部分越大,小數(shù)精度丟失越嚴(yán)重。
精度丟失解決辦法
BigDecimal
Java的在使用除法(divide方法)時(shí),應(yīng)該手動指定精度和舍入的方式。如果不指定精度和舍入方式,在除不盡的時(shí)候會報(bào)異常。
public static void main(String[] args) {
System.out.println("aa= " + new BigDecimal("1.0").divide(new BigDecimal("3.0"),1170,BigDecimal.ROUND_HALF_UP));
}
- BigDecimal舍入規(guī)則查看:舍入規(guī)則
Half
半精度,使用優(yōu)勢:
float16和float相比恰里,總結(jié)下來就是兩個(gè)原因:內(nèi)存占用更少,計(jì)算更快。
-
內(nèi)存占用更少:這個(gè)是顯然可見的,通用的模型 fp16 占用的內(nèi)存只需原來的一半。memory-bandwidth 減半所帶來的好處:
- 模型占用的內(nèi)存更小,訓(xùn)練的時(shí)候可以用更大的batchsize。
- 模型訓(xùn)練時(shí),通信量(特別是多卡,或者多機(jī)多卡)大幅減少,大幅減少等待時(shí)間,加快數(shù)據(jù)的流通。
計(jì)算更快:目前的不少GPU都有針對 fp16 的計(jì)算進(jìn)行優(yōu)化。論文指出:在近期的GPU中,半精度的計(jì)算吞吐量可以是單精度的 2-8 倍
半精度,缺點(diǎn):
- 數(shù)據(jù)溢出問題
- 舍入誤差
BigInteger(不可變的任意精度有符號整數(shù))
這個(gè)應(yīng)該就是表示任意大小的整數(shù)類,里面用了一個(gè)數(shù)組來存儲。


