關於 0.1+0.2 == 0.3 不成立的一些細節

很早之前看到一關於js的問題,如下

實際上 0.1+0.2 != 0.3 這個問題不是js特有,來看一段java代碼

    @Test
    public strictfp void test() {
        System.out.println(0.1f + 0.2f == 0.3f);
        System.out.println(0.1 + 0.2 == 0.3);
    }

程序的運行結果是

true
false


這個問題應該是基礎中的基礎,衆所周知,這是一個不可避免的浮點數精度丟失問題,同十進制一樣,二進制中也會存在無限循環小數,而計算機對浮點數的表示通常是用 32 或 64 位的二進制,這使得在二進制的表達上必須採用“截斷”的手段丟掉一些二進制位

下面我來分析爲什麼上面的運行結果是這樣,由於Java不能直接查看浮點數的十六進制表達,所以用c語言來分析

1、IEEE754 標準

IEEE754是最廣泛運用的浮點數表達的格式。Java裏面有一個不怎麼常見的strictfp關鍵字,用於修飾方法,被修飾的方法中的浮點運算都會嚴格遵守IEEE754規範。關於IEE754的細節,可以翻閱相關資料,這裏就討論主要的。

2、IEEE754 標準下浮點數的存儲格式

浮點數的存儲分爲三個部分,s, e, f,對應符號(sign),指數(exp),有效數位(fraction)

對於float,s+e+f = 1+8+23

對於double,s+e+f=1+11+52

來看下面一段代碼

準備兩個函數,用於顯示浮點數的二進制表達

void float_hex(float* f) {
	int *p = (int *)f;
	printf("%f -> %08x\n", *f, *p);
}

void double_hex(double* d){
	int *p = (int *)d;
	printf("%lf -> %08x %08x\n", *d, *p, *(p+1));
}

main函數的調用

int main() {
	float f = 0.1f;
	float_hex(&f);
	
	double d = 0.1;
	double_hex(&d);
        return 0;
}
運行結果

3dcccccd 對應的二進制
0011 1101 1100 1100 1100 1100 1100 1101

1)規範化表達

下面來驗證一下,對於0.1,轉化成二進制表達

0.000011001100110011....

規範化表達(類似於十進制的科學記數法)

1.100110011001100 x 2^-4

其中符號位0。

2)指數的表達

接下來的指數部分應該是-4,可是實際上存儲的是是0111 1011,實際上這是移碼的表達,移的是127,也就是指數部分存儲的最終內容是127+(-4)=123,也就是0111 1011

3)有效部分截斷時的四捨五入

有效數位部分按道理來講是 1100 1100 1100 的循環直至截斷,由於規範化表達,使得小數點左邊的一位一定是1,所以這一位被省略了,剩下的就是1001 1001 1001的循環,由於有效只能表達23位,所以應該是(灰色表達要被丟棄的部分)

100 1100 1100 1100 1100 1100 1100 1100...

細心的人會注意到最後,四位並不是1100,而是1101,這是因爲截斷尾數時採用了四捨五入的方式,如果截斷的部分第一位爲1,說明截斷部分的數值大於或等於上一位的1/2,因此向前進一位所表達的誤差會更小,因此被截斷的 1100 1100 1100.. 會對上一位產生進位影響,所以最後四位是1100 + 1 = 1101

對於double型也是一樣的方式,值得注意的是代碼中顯示的是 9999 9999a 3fb9 9999,

但實際上這個double的表達是 3fb9 9999 9999 9999a,代碼打印的結果是因爲這裏採用大端存儲的緣故


3、浮點數加法

指數不同的兩個浮點數是不能直接相加的,拿

0.2的有效數位表達和0.1是相同的,但是指數部分比0.1的指數多1,因爲恰好0.1x2=0.2,也可以寫代碼驗證一下。

0.1的指數部分是-4,0.2指數部分是-3,小階要向大階“看齊”,0.1若要表示成指數爲-3,就需要將有效數位整體右移,這樣一來就能對有效數位相加

 1.100 1100 1100 1100 1100 1101 0

 0.110 0110 0110 0110 0110 0110  

______________________________________

10.011 0011 0011 0011 0011 0011 1

橙色部分不會因爲右移而丟失,因爲在計算浮點數的時候會運用兩個比float位數多的臨時變量來計算,double也是

規範化,四捨五入後尾數爲

001 1001 1001 1001 1010

指數爲-2,表達出來是 0111 1101,0.1+0.2的結果應該表達爲

0011 1110 1001 1001 1001 1001 1001 1010(3E99 999A)

4、爲什麼 0.1f+0.2f == 0.3f 成立

上面分析的是0.1+0.2都是浮點數表達,即0.1f+0.2f,結果是3E99 999A,直接用浮點數表達0.3f,也會得到結果3E99 999A,所以Java代碼中第一個雙等號的結果是true。因此,這個 true 的產生是二者截斷後恰好都有進位的原因,並不是精確意義上的相等。

5、爲什麼0.1+0.2 == 0.3 不成立

用同樣的方式分析double,給double型變量直接賦值 0.3 和賦值 0.1+0.2 會得到不一樣的結果,具體原因是 0.1 + 0.2的時候尾數截斷產生了進位,而直接表達 0.3 的時候沒有,詳細流程可以自己分析一下,這裏不贅述。

因此Java代碼中第二個雙等號的結果是false


6、判斷浮點數的運算結果

如果要想判斷0.1+0.2==0.3,應該寫成形如 Math.abs(0.1+0.2 - 0.3) < 1e-6 的形式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章