淺析Golang中float64的精度問題
現(xiàn)象
func main() { x := uint64(1) for i := 0; i < 53; i++ { x = x * 2 } fmt.Println("2^53 =", x) xStr := strconv.FormatUint(x, 10) fmt.Println("len(2^53) =", len(xStr)) xAdded1 := x + 1 fmt.Println("2^53 + 1 =", xAdded1) // 9007199254740993 fmt.Println("float64(2^53 + 1) =", float64(xAdded1)) // 9.007199254740992e+15,出現(xiàn)了精度問題 xAdded2 := x + 2 fmt.Println("2^53 + 2 =", xAdded2) // 9007199254740994 fmt.Println("float64(2^53 + 2) =", float64(xAdded2)) // 9.007199254740994e+15,沒有出現(xiàn)精度問題 fmt.Println("math.MaxInt64 =", int64(math.MaxInt64)) fmt.Println("float64(math.MaxInt64) =", float64(math.MaxInt64)) // 精度問題 fmt.Println("math.MaxUint64 =", uint64(math.MaxUint64)) }
運行結(jié)果:
2^53 = 9007199254740992
len(2^53) = 16
2^53 + 1 = 9007199254740993
float64(2^53 + 1) = 9.007199254740992e+15
2^53 + 2 = 9007199254740994
float64(2^53 + 2) = 9.007199254740994e+15
math.MaxInt64 = 9223372036854775807
float64(math.MaxInt64) = 9.223372036854776e+18
math.MaxUint64 = 18446744073709551615
分析
可以看到float64
無法精確存儲2^53 + 1
,但能精確存儲2^53 + 2
,為什么?
首先,float64
的尾數(shù)位有52位,尾數(shù)的最大長度只能是52+1=53
位,尾數(shù)長度超過53就無法精確存儲,會存在精度問題。
無法精確存儲2^53+1
:
2^53+1
的二進(jìn)制是10000000....0001(中間有52個0)
,根據(jù)IEEE標(biāo)準(zhǔn)則是(-1)^0 * 1.000000000...01(中間有52個0) * 2^53
,尾數(shù)長度是54,超過了53,因此float64
無法存儲第54位,只能舍去最后的1,所以存在精度問題。
能精確存儲2^53+2
:
2^53+2
的二進(jìn)制是1000000...010(中間51個0)
,根據(jù)IEEE標(biāo)準(zhǔn)則是(-1)^0 * 1.00000000...01(中間是51個0)* 2^53
,尾數(shù)長度是53,float64
的尾數(shù)位可以精確存儲,因此沒有精度問題。
知識補充
計算機的浮點數(shù)表示
科學(xué)計數(shù)法和IEEE標(biāo)準(zhǔn)
在計算機中,是用二進(jìn)制的科學(xué)計數(shù)法來表示和存儲浮點數(shù)的。因為科學(xué)計數(shù)法可以唯一地表示任何一個數(shù),且所占用的存儲空間會更少。
比如:對于一個二進(jìn)制數(shù)100000...000(共127個0)
,如果不用科學(xué)計數(shù)法,需要16個字節(jié)來存儲。如果用科學(xué)計數(shù)法:1*2^127,只需要用二進(jìn)制表示出:有效數(shù)字和指數(shù)即可,壓根不需要16個字節(jié)。
IEEE浮點標(biāo)準(zhǔn)用V=(-1)^s * M * 2^E
的形式來表示一個數(shù):
- 符號:s決定該數(shù)是正數(shù)還是負(fù)數(shù)(0正1負(fù)),而對于數(shù)值0的符號位解釋作為特殊情況處理。
- 尾數(shù)(相當(dāng)于有效數(shù)字):M是一個二進(jìn)制小數(shù),對于規(guī)格化表示:1<=M<2
- 階碼(相當(dāng)于指數(shù)):階碼E決定了二進(jìn)制小數(shù)的小數(shù)點位置。(可為負(fù)數(shù))
將一個浮點數(shù)的表示轉(zhuǎn)成如上形式,然后分別對符號、尾數(shù)和階碼進(jìn)行編碼就能得到浮點數(shù)的機器表示。
如下,IEEE標(biāo)準(zhǔn)規(guī)定:
- 對于單精度浮點數(shù),1位符號位+8位階碼位+23位尾數(shù)位。
- 對于雙精度浮點數(shù),1位符號位+11位階碼位+52位尾數(shù)位。
IEEE標(biāo)準(zhǔn)規(guī)定:階碼位表示的是無符號數(shù)e,階碼E
和無符號數(shù)e
的關(guān)系是:E = e - (2^(n-1) - 1)
。
比如,對于單精度浮點數(shù)(8位階碼位),無符號數(shù)e的范圍是[0, 255],因此E = e - (2^7 - 1) = e - 127
,所以階碼E的范圍是[-127, 128],即指數(shù)的范圍是[-127, 128]。
尾數(shù)的規(guī)格化表示: 尾數(shù)M必須1<=M<2
。
為什么要規(guī)格化?保證浮點數(shù)有唯一的表示。若不對浮點數(shù)的表示作出明確規(guī)定,同一個浮點數(shù)的表示就不是唯一的,比如對于十進(jìn)制數(shù)1.75表示可能有1.11*2^0、0.111*2^1、0.0111*2^2等。
規(guī)格化表示后,尾數(shù)一定是1.xxxx
,由于第一位一定是1,所以不需要顯式地表示它,因此尾數(shù)位全部用來表示尾數(shù)1.xxxx
之后的xxxx
部分,也就是說【尾數(shù)的長度(去除小數(shù)點)】最多是【尾數(shù)位+1位】,尾數(shù)超過這個長度之后的數(shù)字位都會被舍棄,從而出現(xiàn)精度問題。
示例
將如下十進(jìn)制數(shù)轉(zhuǎn)成單精度浮點數(shù)表示:
- 1.5
- -12.5
對于1.5,其二進(jìn)制小數(shù)是1.1
,按照IEEE標(biāo)準(zhǔn)轉(zhuǎn)成V=(-1)^s * M * 2^E
的形式,即(-1)^0 * 1.1 * 2^0
,所以:s=0;M=1.1;E=0。然后分別用1位符號位、8位階碼位和23位尾數(shù)位進(jìn)行編碼:
- 因為是正數(shù),所以單精度浮點數(shù)的1位符號位為
0
- 尾數(shù)是
1.1
,尾數(shù)位只需存儲1.1
之后的1
,因此單精度浮點數(shù)的23位尾數(shù)位就是10000000000000000000000
- 階碼E=0,則
e=E+127=127
,因此階碼位對應(yīng)的無符號數(shù)e是127
,所以單精度浮點數(shù)的8位階碼位是01111111
最終,二進(jìn)制表示為00111111110000000000000000000000
同理,對于-12.5,其二進(jìn)制小數(shù)是-1100.1
,即(-1)^1 * 1.1001 * 2^3
,所以:s=0;M=1.1001;E=3。然后分別用1位符號位、8位階碼位和23位尾數(shù)位進(jìn)行編碼:
- 因為是負(fù)數(shù),所以符號位是
1
- 尾數(shù)是
1.1001
,尾數(shù)位只需存儲1.1001
之后的1001
,所以尾數(shù)位是10010000000000000000000
- 階碼E=3,則
e=E+127=130
,因此階碼位對應(yīng)的無符號數(shù)e是130,則階碼位是10000010
最終,二進(jìn)制表示為11000001010010000000000000000000
精度問題
單精度浮點數(shù)有23位尾數(shù)位,最多只能表示23+1=24
位長度的尾數(shù);雙精度浮點數(shù)有52位尾數(shù)位,最多只能表示52+1=53
位長度的尾數(shù)。 也就是說:
- 對于單精度浮點數(shù),若尾數(shù)的長度超過24位,24位之后的數(shù)字就無法存儲,會出現(xiàn)精度問題
- 對于雙精度浮點數(shù),若尾數(shù)的長度超過53位,53位之后的數(shù)字就無法存儲,會出現(xiàn)精度問題
到此這篇關(guān)于淺析Golang中float64的精度問題的文章就介紹到這了,更多相關(guān)Go float64精度內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用go實現(xiàn)一個超級mini的消息隊列的示例代碼
本文主要介紹了使用go實現(xiàn)一個超級mini的消息隊列的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12在Visual Studio Code中配置GO開發(fā)環(huán)境的詳細(xì)教程
這篇文章主要介紹了在Visual Studio Code中配置GO開發(fā)環(huán)境的詳細(xì)教程,需要的朋友可以參考下2017-02-02詳解Golang如何優(yōu)雅判斷interface是否為nil
這篇文章主要為大家詳細(xì)介紹了Golang如何優(yōu)雅判斷interface是否為nil的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解下2024-01-01