最近偶然發(fā)現(xiàn)了0.1+0.2 = 0.30000000000000004的現(xiàn)實(shí),當(dāng)然類似于此的還有很多砰粹,比如0.7*n結(jié)果很多都是一個無限小數(shù)庐扫,本來以為這是千萬個js的坑之一炼杖,但是后來發(fā)現(xiàn)很多語言都有這個問題泳桦,這個問題并不是js的機(jī)制所導(dǎo)致的蜗字,而是所有語言的浮點(diǎn)數(shù)標(biāo)準(zhǔn)IEEE 754所導(dǎo)致的打肝,所以,這篇文章想要大致分享一下探索過程以及究竟為什么會出現(xiàn)這樣的情況挪捕。
這篇文章的主題就是:垃圾浮點(diǎn)數(shù)標(biāo)準(zhǔn)粗梭,自帶誤差,所以算出來不對不能怪電腦<读恪6弦健(好了完了,誤)其實(shí)看了好久這個奏纪,本以為一下午就可以解決的事情鉴嗤,折騰了幾天的感覺(當(dāng)然主要也有工作穿插的原因嘛),最終雖然模模糊糊理解了大致的原因序调,但是不知道自己能不能真正的解釋清楚醉锅。嗯嗯,當(dāng)然浮點(diǎn)數(shù)不是垃圾发绢,IEEE 754標(biāo)準(zhǔn)也是集結(jié)了前人智慧的硬耍,下面進(jìn)入正題。
分析這個問題边酒,其實(shí)需要了解原碼经柴,反碼,補(bǔ)碼等概念墩朦,然后會根據(jù)這些數(shù)碼進(jìn)行二進(jìn)制的運(yùn)算坯认,然后再從這些部分延申到二進(jìn)制浮點(diǎn)數(shù),再逐步分析浮點(diǎn)數(shù)的加減。但是這樣其實(shí)有點(diǎn)繁瑣牛哺,整理起來也沒那么簡單陋气,所以我們首先聚焦這個問題:0.1+0.2的結(jié)果按道理來說是0.3,但是為什么這里會有溢出呢荆隘?我們剛才也說到了IEEE 754標(biāo)準(zhǔn),那么我關(guān)注的就是這個標(biāo)準(zhǔn)赴背,浮點(diǎn)數(shù)在這個標(biāo)準(zhǔn)之下是怎樣進(jìn)行存儲的椰拒,導(dǎo)致了有溢出出現(xiàn)呢?
我們都知道凰荚,在計(jì)算機(jī)之中燃观,所有的數(shù)據(jù)都是二進(jìn)制存儲的,比如說數(shù)字5便瑟,二進(jìn)制表示為101缆毁,數(shù)字15,二進(jìn)制表示為1111到涂。而且在計(jì)算機(jī)中,數(shù)字的表示還是有一定格式的践啄,也就是精度。比如屿讽,有數(shù)據(jù)類型的限制是用8位表示,而且又要分正負(fù)伐谈,那么它的取值范圍就是-2^7 ~ 2^7-1(c++中的char類型烂完,就是這樣的取值范圍,有人可能會疑惑為什么不是-128 ~ 128诵棵,這個稍后再講)。所以如果用這樣的類型表示數(shù)字履澳,5就是00000101,15就是00001111奇昙,采用高位用0補(bǔ)齊的方式存儲到內(nèi)存當(dāng)中护侮。
那浮點(diǎn)數(shù)怎么辦呢?諸如20.4储耐,30.67羊初,0.5等等或簡單或復(fù)雜的小數(shù),我們首先發(fā)現(xiàn)的問題是小數(shù)位該怎么去表示长赞。我們在中學(xué)中有學(xué)到整數(shù)的表示法可以稱為“除2取余法”,比如5/2 = 2 ……1得哆,2/2 = 1……0,1/2 = 0 …… 1贩据,所以5表示為101,這么做的原因就是5可以表示為12^2+021+1*20矾芙。那么我們反過來思考小數(shù),是不是也可以表示成2e相加(e為負(fù)數(shù))的形式剔宪,但是遺憾的是,2-1 = 0.5葱绒, 2^-2 = 0.25斗锭,不像2的正數(shù)次方那么有規(guī)律哈街,所以注定有些數(shù)我們是表示不了的拒迅。但是作為一個完整的運(yùn)算系統(tǒng),這些數(shù)字我們是不可能舍棄的呀璧微,所以我們只能近似的取到這些數(shù)字。
等等胞得,我們好像還沒說浮點(diǎn)數(shù)該怎么表示屹电,怎么好像已經(jīng)發(fā)現(xiàn)了浮點(diǎn)數(shù)會出問題的原因了阶剑。其實(shí)只想知道大致為什么的同志危号,到這里就可以over了,其核心原因就是外莲,本身用二進(jìn)制來表示小數(shù)就會難以覆蓋猪半,所以采用了一種近似的方式兔朦,既然是近似,那么有誤差的出現(xiàn)磨确,似乎也沒那么奇怪了。下面將結(jié)合我的了解和資料的查詢分析乏奥,探索一下究竟是怎樣的二進(jìn)制存儲與運(yùn)算才導(dǎo)致了這樣的情形。
首先就是剛才還未說完的二進(jìn)制表示浮點(diǎn)數(shù)恨诱,對于小數(shù)部分驶悟,我們需要使用“乘2取整法”材失,例如0.875
0.875*2 = 1.75 整數(shù)部分 1
0.75 * 2 = 1.5 整數(shù)部分 1
0.5 * 2 = 1.0 整數(shù)部分 1
所以0.875的二進(jìn)制的小數(shù)部分表示就是 111,我們逆向計(jì)算一下龙巨,12^-1 + 12^-2 + 1*2^-3 = 0.875
從理論數(shù)據(jù)上我們看到,這樣做是沒錯的旨别,雖然我們選擇的數(shù)據(jù)可能有點(diǎn)那么“正正好”。
那么接下來我們就碰到了下一個嚴(yán)峻的問題铭若,小數(shù)點(diǎn)怎么辦递览?二進(jìn)制是沒有辦法表示小數(shù)點(diǎn)的呀,那就輪到我們的IEEE 754登場了绞铃。在計(jì)算機(jī)中,浮點(diǎn)數(shù)(此處以單精度float32位為例荚坞,當(dāng)然js使用的也是這一個菲盾,在c++等語言中還有雙精度double64位颓影,這個位就是剛才我所說的精度的概念)采用了一種特別的方式去保存懒鉴,在涉及到小數(shù)位的時候,你需要先把小數(shù)轉(zhuǎn)換為二進(jìn)制向上面那樣咆畏,0.875轉(zhuǎn)換成了0.111,然后通過移位讓數(shù)字的整數(shù)部分為1旧找,形成1.xxxxx * 2e的形式,所以0.111就可以表示成1.11*2-1钮蛛。在IEEE 754之中,浮點(diǎn)數(shù)的存儲分為三個部分岭辣,在各種文獻(xiàn)中的解釋極其正規(guī)的解釋了三部分叫做甸饱,sign bit,exponent bias叹话,fraction,emmm大致是如下結(jié)構(gòu)
他們各自的命名是符號為氏豌,偏移量(移碼热凹,階碼)
注:
原碼是一個數(shù)的二進(jìn)制,而反碼是這個數(shù)對于當(dāng)前數(shù)位的滿值的補(bǔ)值般妙。啊,這句話說的我自己都不理解什么意思霹陡,舉個例子(一下用四位進(jìn)行表示止状,一位符號位烹棉,三位真值域):
a = 2 (0010)
a取反怯疤,a的反碼0101,記作b=5
a+b = 0111 = 7伏社,為當(dāng)前數(shù)位的滿值,即2^n-1
這個道理其實(shí)恰恰印證了模運(yùn)算的合理性與應(yīng)用在二進(jìn)制運(yùn)算上的正確性