緣起:
公司一小姑娘群發(fā)郵件求助毁葱,郵件內(nèi)容是關(guān)于浮點(diǎn)數(shù)之間運(yùn)算結(jié)果莫名其妙的多了一些小數(shù)位奕坟,小姑娘想不通所以想問(wèn)問(wèn)有人知道原因不距潘?涉及的代碼是下面的樣子
function addTwoFloatString(stra , strb){
return parseFloat(stra) + parseFloat(strb);
}
addTwoFloatString('2222222','269567.53');
->2491789.5300000003
大家看到這個(gè)問(wèn)題后獻(xiàn)計(jì)獻(xiàn)策甜熔,有說(shuō)不應(yīng)該那樣蜓氨,應(yīng)該這樣
Number( 123.5678.toFixed(2) )
還有說(shuō)應(yīng)該用BigDecimal做浮點(diǎn)數(shù)相加運(yùn)算的聋袋。
看到這個(gè)求助后,直覺(jué)告訴我這個(gè)和浮點(diǎn)數(shù)的表示法肯定有關(guān)系穴吹,至于為什么幽勒,詳細(xì)的答案是真說(shuō)不好。如果非要搪塞一下港令,我會(huì)說(shuō)浮點(diǎn)數(shù)之間的運(yùn)算會(huì)有精度問(wèn)題啥容,如果深究為什么,那就糗了顷霹。
驗(yàn)證
首先看看是否其他語(yǔ)言中也存在類(lèi)似的問(wèn)題咪惠,ok,java中也存在同樣的問(wèn)題淋淀,那么基本可以斷定這個(gè)現(xiàn)象和編程語(yǔ)言關(guān)系不大遥昧。
接著需要確認(rèn)的問(wèn)題是,JavaScript中浮點(diǎn)數(shù)是如何定義的:
JavaScript中的浮點(diǎn)數(shù)采用IEEE-754格式的規(guī)定。更具體的說(shuō)是一個(gè)雙精度格式炭臭,這意味著每個(gè)浮點(diǎn)數(shù)占64位叫乌。雖然它不是二進(jìn)制表示浮點(diǎn)數(shù)的唯一途徑,但它是目前最廣泛使用的格式徽缚,另外憨奸,JavaScript中的數(shù)值不區(qū)分整數(shù)值和浮點(diǎn)數(shù)值,所有數(shù)字都采用的是64位浮點(diǎn)數(shù)表示法凿试。
似乎看到了希望排宰,IEEE754
wiki中對(duì)IEEE754中64位浮點(diǎn)數(shù)有下面的描述,總長(zhǎng)度占用64位那婉,其中1為符號(hào)位板甘,標(biāo)明浮點(diǎn)數(shù)是正數(shù)還是負(fù)數(shù),11位為指數(shù)為详炬,52位為尾數(shù)位盐类。
知道這些現(xiàn)在還不能準(zhǔn)確的表述一個(gè)數(shù),得有一個(gè)約定呛谜,約定尾數(shù)的形式在跳,不然同一個(gè)數(shù)字就有多種表示,這個(gè)過(guò)程稱為規(guī)格化隐岛,形象化的描述就是一個(gè)數(shù)需要表示成這樣的格式: 1.M* 2E猫妙,這樣的數(shù)叫做規(guī)格化數(shù)。
另外聚凹,IEEE還規(guī)定了指數(shù)的存儲(chǔ)形式割坠,64位浮點(diǎn)數(shù)指數(shù)部分以偏正值形式表示,偏正值為實(shí)際的指數(shù)大小與1023的和妒牙。
到此彼哼,就可以將文章開(kāi)頭的兩個(gè)數(shù)用IEEE754的定義表示出來(lái)。
對(duì)2222222做規(guī)格化湘今, 符號(hào)位為0敢朱,表示是一個(gè)正數(shù),2222222對(duì)應(yīng)的二進(jìn)制是1000011110100010001110象浑,有22位蔫饰,滿足規(guī)格化需小數(shù)點(diǎn)左移21為,指數(shù)就為21 +1023 = 1044愉豺,尾數(shù)為1.000011110100010001110
最終2222222 的64位二進(jìn)制浮點(diǎn)數(shù)就表示為
0 10000010100 0000111101000100011100000000000000000000000000000000
269567.53的64位浮點(diǎn)數(shù)就表示為
0 10000010001 0000011100111111111000011110101110000101000111101100
Java中有對(duì)應(yīng)的函數(shù)可以將根據(jù)IEEE 754規(guī)定篓吁,返回指定浮點(diǎn)值的表示形式
Long.toBinaryString(Double.doubleToRawLongBits(2222222))
接下來(lái)就是浮點(diǎn)數(shù)加減法運(yùn)算步驟了,如下
對(duì)階蚪拦,使兩個(gè)數(shù)的小數(shù)點(diǎn)位置對(duì)齊
尾數(shù)求和杖剪,將運(yùn)算后的兩個(gè)尾數(shù)求和
將求和后的尾數(shù)規(guī)格化
舍入冻押,考慮尾數(shù)右移時(shí)的丟失的數(shù)值位
判斷結(jié)果是否溢出
-
對(duì)階的原則是小階向大階看齊,階碼較小的數(shù)尾數(shù)向右移盛嘿,每移一位洛巢,階碼加一,在269567.53 + 2222222中次兆,前者向后者看齊稿茉,前者尾數(shù)右移三位。對(duì)階后兩數(shù)分別為
010000010100 0010000011100111111111000011110101110000101000111101100 010000010100 0000111101000100011100000000000000000000000000000000
-
尾數(shù)相加
0100000101000010000011100111111111000011110101110000101000111101100 0100000101000000111101000100011100000000000000000000000000000000 0100000101000011000000101100011011000011110101110000101000111111100
規(guī)格化
滿足規(guī)格化
-
舍入
0100000101000011000000101100011011000011110101110000101000111111100
浮點(diǎn)數(shù)舍入的方式有多種芥炭,但是不可避免的漓库,都可能會(huì)發(fā)生數(shù)位的丟失,這個(gè)就是直接導(dǎo)致浮點(diǎn)數(shù)之間運(yùn)算后出現(xiàn)精度問(wèn)題的直接原因园蝠。
java和javascript中最終舍入后的表示形式為
0100000101000011000000101100011011000011110101110000101000111110
結(jié)論
由于二進(jìn)制表示本身的缺陷渺蒿,我們不得不面對(duì)一個(gè)充滿舍入誤差的規(guī)范,進(jìn)而導(dǎo)致計(jì)算結(jié)果超乎預(yù)期彪薛。