浮點數精度之謎

2021-09-11 10:49:41 字數 3787 閱讀 4290

話要從業務**裡的bug說起,大致過程是前端運算 2.07-1 之後結果卻是1.0699999999999998,老司機們都知道是浮點數運算的精度丟失導致的,在檢視了下具體**,果然處理不當。因此我深究一番,並誕生了此文。此處重點強調兩個認識誤區:

首先不得不說說浮點數的表示方法,任何數在計算機面前都會被處理成二進位制,而數字的二進位制表示主要有原碼、反碼、補碼。(有點熟悉對不對?哥就是來給你補計算機組成原理的,壞笑~)

原碼原碼是計算機中對數字的二進位制的定點表示方法,最高位表示符號位,其餘位表示數值位。優點顯而易見,簡單直觀;缺點也很明顯,不能直接參與運算,可能會報錯,如11+(-11) => 10010110 => -22,結果竟然不等於0。(臥槽,瞎搞啊~,以為我沒上過學?)所以,原碼符號位不能直接參與運算。說到這,給大家個思考題,8位有符號的原碼表示範圍是多少?自己思考哈~

反碼正數的反碼和其原碼一樣;負數的反碼,符號位為1,數值部分按原碼取反。例如 [+7]原 = 00000111,[+7]反 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000。

補碼正數的補碼和其原碼一樣;負數的補碼為其反碼加1。例如 [+7]原 = 00000111,[+7]反 = 00000111,[+7]補 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000,[-7]補 = 11111001。

說到這,你也許會問,哥你這都是講的整數啊,沒說到浮點數啊。別急,弟繼續往下看~

浮點數的表示方法

國際標準ieee 754規定,任意乙個二進位制浮點數v都可以表示成下列形式:

(-1)^s 表示符號位,當s=0,v為整數;s=1,v為負數;

m 表示有效數字,1≤m<2;

2^e 表示指數字

舉個小栗子?:

-0.5 => -0.1[二進位制]

=> -1.0 * 2^-1

=> (-1)^1 * 1.0 * 2^-1

=> s=1,m=1.0,e=-1

ieee 754又規定了,浮點數分單精度雙精度之分:

對於有效數字m和指數e,這個ieee 754還規定了:

有效數字m

(1)1≤m<2,也即m可以寫成1.***xx的形式,其中***xx表小數部分

(2)計算機內部儲存m時,預設這個數第一位總是1,所以捨去。只儲存後面的***xx部分,節省一位有效數字

指數e(階碼)

(1)e為無符號整數。e為8位,範圍是0~255;e為11位,範圍是0~2047

(2)因為科學計數法中的e是可以出現負數的,所以ieee 754規定e的真實值必須再減去乙個中間數(偏移值),127或1023

有人又要問了,哥,為啥子要有中間數?自己思考哈,弟你自己要學會成長,實在不行你也可以問你谷哥~

attention! 精華部分來了~

浮點數加法

浮點數的加法運算(不要問哥為啥只講加法~)分為下面幾個步驟:

(1)對階

顧名思義就是對齊階碼,使兩數的小數點位置對齊,小階向大階對齊;

(2)尾數求和

對階完對尾數求和

(3)規格化

尾數必須規格化成1.m的形式

(4)捨入

在規格化時會損失精度,所以用捨入來提高精度,常用的有0舍1入法,置1法

(5)校驗判斷

最後一步是校驗結果是否溢位。若階碼上溢則置為溢位,下溢則置為機器零

例項計算(以單精度為例)

0.2 => 1/8 + 1/16 + 1/128 +... => 1.100110011001100...*2^-3 =>

0(符號位) 01111100 (指數字) (1) 10011001100110011001100(尾數字)

0.4 => 1/4 + 1/8 +1/64 +... => 1.100110011001100...*2^-2 =>

0(符號位) 01111101 (指數字) (1) 10011001100110011001100(尾數字)

這裡,細心的同學可能會發現指數字為何是01111100,不是應該是-3,這是因為-3加上了中間值127等於124;所以反算的時候,要用計算值減去中間值得到真正的指數值。

(1)對階

根據小階對大階原則,0.2的階碼向0.4階碼對齊,即0.4的階碼不作調整,0.2的階碼對齊,且尾數做右移處理:

0.2 => 0 01111101 (0)11001100110011001100110

0.4 => 0 01111101 (1)10011001100110011001100

(2)尾數求和

(0)11001100110011001100110 + (1)10011001100110011001100 => (10)01100110011001100110010

(3)尾數規格化

0 01111101 (10)01100110011001100110010 => 0 01111110 (1)00110011001100110011001

⚠️ 最後的0被移出去了,這就是誤差產生的根源!

(4)捨入

(5)校驗判斷

0.2 + 0.4 => 0 01111110 (1)00110011001100110011001 => 1.1999999285/2 => 0.5999999643 (並不等於0.6)

最後發現計算結果果然出現誤差,因為在尾數規格化的步驟中可能產生移位誤差,看來要想精確運算,不能直接操作浮點數運算啊!最保險的方法是在運算過程中,將浮點數處理成整數進行運算:

/**

* [scalenum 通過操作其字串將乙個浮點數放大或縮小]

* @param num 要放縮的浮點數

* @param pos 小數點移動位數

* pos大於0為放大,小於0為縮小;不傳則預設將其變成整數

* @return 放縮後的數

*/function

scalenum(num, pos)

let parts = num.tostring().split('.');

const intlen = parts[0].length;

const decimallen = parts[1] ? parts[1].length : 0;

// 預設將其變成整數,放大倍數為原來小數字數

if (pos === undefined) else

if (pos > 0)

} else

}const idx = intlen + pos;

parts = parts.join('').split('');

parts.splice(idx > 0 ? idx : 0, 0, '.');

return

parsefloat(parts.join(''));

}複製**

有很多同學將浮點數擴大成整數,直接乘以10^n,其實這也會可能導致誤差,例如 0.57*100 => 56.99999999999999;另外除法運算也可能導致誤差,5.7/10 => 0.5700000000000001;記住,包含浮點數的加減乘除都可能導致計算誤差。

q&a:

8位有符號的原碼表示範圍是多少?

a:111111111 ~ 01111111 => -127 ~ +127

階碼運算為啥要有中間數?

a:指數可以為正數,也可以為負數。為了計算機處理資料的方便,就是希望在加法運算中將減法運算一併處理了,所以處理了負指數的情況,加上中間值來簡化cpu中運算器的設計

浮點數精度問題

一 例子 首先我們去編譯器試試 double a 1.9 通過新增監視檢視a的值 會發現a的值是1.8999999 二 開始今天的學習 在最開始學c 的時候並沒有對浮點數進行很深入的學習,認為浮點不就是小數嘛,首先在c 的巨集裡面有 flt max 和 flt min 的定義,float是四位元組的...

單雙精度浮點數

浮點數並不一定等於小數 定點數也並不一定就是整數。所謂浮點數就是小數點在邏輯上是不固定的,而定點數只能表示小數點固定的數值,具用浮點數或定點數表示某哪一種數要看使用者賦予了這個數的意義是什麼。c 中的浮點數有6種,分別是 float 單精度,32位 unsigned float 單精度無符號,32位...

浮點數精度丟失問題

c 中的浮點數,分單精度 float 和雙精度 double float 是 system.single 的別名,介於 3.402823e38 和 3.402823e38 之間的32位數字,符合二進位制浮點演算法的 iec 60559 1989 ieee 754 標準 double 是 system...