浮點數的這些坑,你未必知道

猿界汪汪隊發表於2021-11-04
我猜作為開發工程師,大部分人都用過浮點數。但是你是否用對了呢?你是否知道,浮點數有近一半的值,在-1與+1以內呢?
 
本節大綱有:
1、基本資料型別
2、認識浮點數規律
3、我在浮點數上踩過的坑
 
在計算機的眼中,一切都是數字,一切都是二進位制。

一、基本資料型別

關於數值,你應該時刻牢記在心裡的三點,
1、每個基本型別的數值都是有範圍限制的,不是無限大的。
2、無論是boolean,int,float,string,struct,object,最終都對應計算機的一個byte或者多個byte。
3、非必需,不要使用浮點數。
 
下面說幾個在工作中會經常遇到的幾個問題:
1、計算機中32位的有符號整型int,其最大值就是2147483647,這個數大約是21億多。如果放普通計算應該都不成問題。但是,如果你資料庫表的主鍵是int型別,那你就要小心了,好多大廠都在這個資料型別上面栽過跟頭。因為現在中國網民基數太大,一張表很容易上億,一旦超過21億多,你的系統可能會因為db主鍵溢位導致故障。
2、在金融行業應該注意數值溢位的問題。兩個integer相加或者相乘,其結果值很有可能超過integer的表示範圍。在金融這種對精確度要求特別高的行業,這種錯誤更是不可忍受的。
3、返回給前端的json資料,儘量不要使用數值型別。如果你做過前端相關的專案,你會發現,當你使用json格式字串給前端返回數值型別時,前端展示出來的數字可能跟你返回的數字完全不一樣。其根本原因就是Javascript的浮點數沒有辦法精確表示較大數值。
 
 
 

二、認識浮點數規律

如果你沒有耐心去深入理解浮點數(說句實話,確實複雜),那麼我建議你至少應該記住關於浮點數的幾個規律和建議。
  1. Javascript中的數字都是雙精度浮點數。
  2. 在浮點數中,0有+0和-0兩種表示方法
  3. 浮點數表示的值是不連續的,不均勻的。越大的數,越無法用浮點數表示出來。
  4. 有近一半的浮點數數值分佈在-1和+1之間。
  5. 不要用浮點數來生成隨機數。
  6. 不要用浮點數來儲存或計算有關金錢和資產方面的一切,包括常見的會員積分。不能用浮點數用於任何需要精確值的運算,因為浮點數無法精確的表示數字,比如說0.1就無法用浮點數精確表示出來。
規律論證:
有關浮點數詳細教程,大家可以自己百科(IEEE754標準)。32位的浮點數長度數值太多,不好理解,我們用8bit長的浮點數來學習,學習得出的規律,對32位和64位浮點數同樣適用。
8bit浮點數,定義階碼E為4bit,尾數M為3bit,符號位1位。
8bit的浮點數,其能表示的數值只有256個,全部列出來仍然很多,我們把其中不太重要的數值省略掉。
8位浮點數256種狀態表示值如下(下圖出自Computer Systems: A Programmer's Perspective)。

 圖一

上圖列出的是正浮點數,即只列出了0及以上127個數值部分。
其中第二列為二進位制表示。從上至下是連續增長的二進位制,一共有128-8=120個狀態值(01111000之後有8個狀態值浮點數沒有使用),但是因為是浮點數,所以其表示的值,需要經過一定計算規則才能得出,最後一列就是二進位制對應十進位制值。
8位浮點數,其所表示的數值範圍是0-240。儘管其二進位制只有120個狀態值,但是其表示的最大值卻達到了240,所以在240以內,一定有很多整數值,無法用8位浮點數表示。所以說,浮點數表示的值,不是連續的。如果將其能夠表現出來的值,使用藍點畫出來,其所表示的數值大概如下:

 圖二

圖出自Computer Systems: A Programmer's Perspective。
從上圖可以看出,浮點數表示的數值,靠近0中間最多,越靠外(數值越大),浮點數數值越少。比如從圖一的表中可以看出,數值224-240之間的整數,都沒有辦法用8位的浮點數表示出來。所以說,越大的數,其用浮點數表示的概率越低。
另外,如果你利用浮點數小數乘以一個倍數來生成整數隨機數,你會發現,你生成的隨機數出現跟浮點數一樣的規律,即生成的隨機數大多數都集中靠近中心0的位置。所以,不要利用浮點數生成隨機數。
 
我們放大-1至+1範圍的數值

 圖三

我們發現在1以內,像0.1,0.2等小數,也是沒有辦法用8位浮點數表示出來的。從這裡也可以看出,浮點數的0有+0和-0之分。
我們使用8位浮點數和32位浮點,計算一下1以內和1以外的規格化數字的大概個數。因為64位太大,所以我這裡就不再計算了,但是其數值個數比例應該是類似的。
浮點數
8位浮點數
32位浮點數
1的二進位制
 0 0111 000
0 0111 1111 000 0000 0000 0000 0000 0000
1以內數值個數
 56
1,065,353,216
最大規格數二進位制
 0 1110 111
0 1111 1110 111 1111 1111 1111 1111 1111
最大規格數表示的數值
 119
 
1以上二進位制個數
 119-56= 63
2,139,095,039 -1,065,353,216=
1,073,741,823
從以上數值計算可以看出,有近一半的浮點數值,在-1和1以內。
 

三、我在浮點數上踩過的坑

作為親身經歷者,我說兩個案例。
第一個案例,使用者返現金額是前端JS計算。我上面說過了,JS中的數值都是雙精度浮點數,正好返現中用到了減法,於是就出現計算出的返現結果是1.199999999結果,然後直接傳給了後端。但是錢的精度是不能超過2位小數的,後端直接將後面的9給截掉了,於是就返給客人的錢就少了1分錢。當然,這都是極少的情況,所以問題隱藏了很久。
第二個案例,後端用double型別來計算退款金額,當一筆交易多次退款的時候,有時出現錢不夠退或者沒有退完的情況。經過多次使用者投訴,最終才修復這個問題;
 
浮點數使用不當的問題,通常情況很難發現。但是隻要存在,就一定會出現,現在沒有遇見,不代表將來也沒有。迄今為止,為浮點數交的學費最貴的當屬歐洲航空總署,因為浮點數轉換不當,導致在1996年6月4號發射阿麗安娜5火箭爆炸,當時的火箭裡面放著一個價值5億美金的衛星。
國內也有好多一些工程師將遇見的問題分享了出來,隨便百度了一個浮點數踩坑案例,https://blog.csdn.net/yangsh3002/article/details/52507097
 
如果你有時間,你可以執行如下Java程式碼,看看效果
public class FloatTest {
    public static void main(String[] args) {
    double a = 1.0/10;
    double b = 1-0.9;
    System.out.println("1.0/10="+a);
    System.out.println("1-0.9="+b);
    System.out.println(a==b);
    }
}

 

在我的電腦上輸出如下
1.0/10=0.1
1-0.9=0.09999999999999998
false

 

相關閱讀:

 
 
 
 
參考資料:

相關文章