原理解析 | JavaScript 計算0.1 + 0.2真的很難,看完才知道!
已經(jīng)很久沒有寫技術(shù)文章了,腦袋瓜有點生銹,寫的不好別見怪,今天就是想帶點干貨給大家分享一下。文章的內(nèi)容有一點點難度,不過基本都是計算機(jī)組成原理的知識,算是溫故而知新吧!
學(xué)過JavaScript的童鞋應(yīng)該非常清楚,0.1 + 0.2 是不等于0.3的,而是等于0.30000000000000004,至于為什么會這樣?好像有點說不清楚,只知道是JavaScript精確度問題,其他不得而知了。沒關(guān)系,看完慢慢就懂了!
目錄
浮點數(shù)十進(jìn)制轉(zhuǎn)二進(jìn)制
IEEE 754 標(biāo)準(zhǔn)
浮點數(shù)二進(jìn)制運(yùn)算
實踐擴(kuò)展
十進(jìn)制轉(zhuǎn)二進(jìn)制
0.1的二進(jìn)制:0.000110011......0011......(0011無限循環(huán))
0.2的二進(jìn)制:0.00110011......0011...... (0011無限循環(huán))
說明:轉(zhuǎn)換過程就不在這邊描述了
IEEE 754 標(biāo)準(zhǔn)
IEEE二進(jìn)制浮點數(shù)算術(shù)標(biāo)準(zhǔn)(IEEE 754)是20世紀(jì)80年代以來最廣泛使用的浮點數(shù)運(yùn)算標(biāo)準(zhǔn),為許多CPU與浮點運(yùn)算器所采用。
這個標(biāo)準(zhǔn)定義了表示浮點數(shù)的格式(包括負(fù)零-0)與反常值(denormal number)),一些特殊數(shù)值(無窮(Inf)與非數(shù)值(NaN)),以及這些數(shù)值的“浮點數(shù)運(yùn)算符”;它也指明了四種數(shù)值舍入規(guī)則和五種例外狀況(包括例外發(fā)生的時機(jī)與處理方式)。百度百科
浮點數(shù)存儲格式

上圖是64位的雙精度浮點數(shù),最高位是符號位S(sign),中間的11位是指數(shù)E(exponent),剩下的52位為尾數(shù)(有效數(shù)字)M(mantissa)。
浮點數(shù)科學(xué)計數(shù)法
根據(jù)IEEE 754標(biāo)準(zhǔn),任意一個浮點數(shù)的二進(jìn)制都可以用如下公式進(jìn)行表示:

S為符號位:表示浮點數(shù)的正負(fù)(0代表正數(shù),1代表負(fù)數(shù));
E為指數(shù)位:存儲指數(shù),該數(shù)都會加上一個常數(shù)(偏移量),用來表示次方數(shù);
M為尾數(shù)位:表示有效位(尾數(shù)),超出的部分自動進(jìn)1舍0;
雙精度的浮點數(shù)真值(帶有正負(fù)號的數(shù)值是真值)最終可以表示為:

說明:E是無符號整數(shù),長度是11位,取值范圍是為0~2047。因為科學(xué)計數(shù)法中的指數(shù)是可以為負(fù)數(shù),所以約定減去一個中間數(shù)(偏移量)1023,[0,1022] 表示為負(fù),[1024,2047] 表示為正。
浮點數(shù)二進(jìn)制運(yùn)算
規(guī)格化
大部分二進(jìn)制浮點數(shù)都以規(guī)格化格式進(jìn)行存放,以便將有效數(shù)字的精度最大化,提升精確度。
0.1的二進(jìn)制
0.000110011001100110011001100110011001100110011001100110011...... (0011無限循環(huán))
0.1科學(xué)計數(shù)法表示
小數(shù)點向右移動4位,讓其小數(shù)點左邊只有一個“1”。
1.10011001100110011001100110011001100110011001100110011......(0011無限循環(huán)) * 2^-4
根據(jù)IEEE754標(biāo)準(zhǔn),雙精度浮點的尾數(shù)只能存儲52位,加粗的“1”是第53位,根據(jù)進(jìn)1舍0的原則進(jìn)行操作,操作后的值為:
1.1001100110011001100110011001100110011001100110011010* 2^-4
0.1二進(jìn)制存儲格式
指數(shù)-4等于1019(E) – 1023(常量),由此可得E等于1019,把1019轉(zhuǎn)為二進(jìn)制1111111011。
最終表示如下:
0,01111111011;1001100110011001100110011001100110011001100110011010
說明:第一部分為符號位;第二部分為指數(shù)位;第三部分為尾數(shù)位;
0.2的二進(jìn)制
0.001100110011001100110011001100110011001100110011001100110011...... (0011無限循環(huán))
0.2科學(xué)計數(shù)法表示
小數(shù)點向右移動3位,讓其小數(shù)點左邊只有一個“1”。
1.10011001100110011001100110011001100110011001100110011......(0011無限循環(huán)) * 2^-3
加粗的“1”是第53位,根據(jù)進(jìn)1舍0的原則進(jìn)行操作,操作后的值為:
1.1001100110011001100110011001100110011001100110011010...... (0011無限循環(huán)) * 2^-3
0.2二進(jìn)制存儲格式
指數(shù)-3等于1020(E) – 1023(常量),由此可得E等于1020,把1020轉(zhuǎn)為二進(jìn)制1111111100。
最終表示如下:
0,01111111100;1001100110011001100110011001100110011001100110011010
說明:第一部分為符號位;第二部分為指數(shù)位;第三部分為尾數(shù)位;
對階
對階的目的是使兩數(shù)的小數(shù)點位置對齊,方便兩數(shù)進(jìn)行運(yùn)算,換句話說就是兩數(shù)的階碼要相等。根據(jù)小階向大階看齊的原則,應(yīng)使0.1的尾數(shù)向右移動1位(可以理解為小數(shù)點向左移動1位),階碼加1。
尾數(shù)向右移動1位后,階碼和尾數(shù)的值變化如下:
階碼:01111111011 + 1 ——> 01111111100
尾數(shù):
11001100110011001100110011001100110011001100110011010
——> 1100110011001100110011001100110011001100110011001101
加粗的“1”是右移補(bǔ)的位,加粗的0是舍去的位,根據(jù)IEEE 754標(biāo)準(zhǔn)雙精度浮點的尾數(shù)只能存儲52位,遵循進(jìn)1舍0的原則進(jìn)行操作。
0.1的科學(xué)計數(shù)法表示:
0.1100110011001100110011001100110011001100110011001101 *2^-3
0.1的二進(jìn)制存儲格式:
0,01111111100;1100110011001100110011001100110011001100110011001101
尾數(shù)求和
尾數(shù)部分M通常都是規(guī)格化表示的,非"0"的尾數(shù)其第1位總是"1",而這一位也稱作隱藏位,因為存儲的時候該位會被省略。比如存儲1.0110時,只存儲尾數(shù)0110,等到讀取的時候才把第1位的1加補(bǔ)上去,這么做相當(dāng)于多保存了1位有效數(shù)字。
0.1的尾數(shù) + 0.2的尾數(shù) =
0.1100110011001100110011001100110011001100110011001101?+?1.1001100110011001100110011001100110011001100110011010?=?10.0110011001100110011001100110011001100110011001100111
由此可得:
0.1 + 0.2 =?10.0110011001100110011001100110011001100110011001100111* 2^-3
結(jié)果規(guī)格化
根據(jù)尾數(shù)求和的結(jié)果,進(jìn)行規(guī)格化處理,即尾數(shù)向右移1位,階碼加1。
1.00110011001100110011001100110011001100110011001100111?* 2^-2
尾數(shù)只能存儲52位,加粗的“1”需要舍去,根據(jù)進(jìn)1舍0的原則進(jìn)行操作可得:
1.0011001100110011001100110011001100110011001100110100* 2^-2
二進(jìn)制存儲格式:
指數(shù)-2等于1021(E) – 1023(常量),由此可得E等于1021,把1021轉(zhuǎn)為二進(jìn)制01111111101;
0,01111111101;0011001100110011001100110011001100110011001100110100
溢出判斷
浮點數(shù)的溢出其實是階碼的溢出表現(xiàn)出來的,在算術(shù)運(yùn)算過程中要檢查是否產(chǎn)生了溢出。若階碼正常,算術(shù)運(yùn)算正常結(jié)束;若階碼溢出,則要進(jìn)行相應(yīng)處理。
如上求和結(jié)果的階碼為01111111101,沒有產(chǎn)生溢出,因此運(yùn)算結(jié)束。
結(jié)果轉(zhuǎn)為十進(jìn)制
二進(jìn)制存儲格式是計算機(jī)存儲和運(yùn)算的格式,此時把二進(jìn)制轉(zhuǎn)為十進(jìn)制,我們可以看看最終求和的值會是多少?
規(guī)格化的值是轉(zhuǎn)為非規(guī)格化
指數(shù)的值為2,將規(guī)格化的小數(shù)點向左移動2位即可。
1.0011001100110011001100110011001100110011001100110100* 2^-2
——>
0.010011001100110011001100110011001100110011001100110100
非規(guī)格化的值轉(zhuǎn)成十進(jìn)制
0.1 + 0.2 =
0.3000000000000000444089209850062616169452667236328125
此時此刻,你應(yīng)該明白JavaScript中0.1 + 0.2 = 0.30000000000000004 這個值是怎么來的吧。
實踐擴(kuò)展
為了方便大家學(xué)習(xí)與實踐,提供如下PHP實踐代碼,它是將“0.010011001100110011001100110011001100110011001100110100”轉(zhuǎn)成十進(jìn)制的功能代碼,大家可以嘗試測試一下。
var_dump(number_format(0+ 1 * pow(2, -2) + 0 + 0 + 1 * pow(2, -5) + 1 * pow(2, -6) + 0 + 0 + 1 * pow(2,-9) + 1 * pow(2, -10) + 0 + 0 + 1 * pow(2, -13) + 1 * pow(2, -14) + 0 + 0 + 1 *pow(2, -17) + 1 * pow(2, -18) + 0 + 0 + 1 * pow(2, -21) + 1 * pow(2, -22) + 0 +0 + 1 * pow(2, -25) + 1 * pow(2, -26) + 0 + 0 + 1 * pow(2, -29) + 1 * pow(2,-30) + 0 + 0 + 1 * pow(2, -33) + 1 * pow(2, -34) + 0 + 0 + 1 * pow(2, -37) + 1* pow(2, -38) + 0 + 0 + 1 * pow(2, -41) + 1 * pow(2, -42) + 0 + 0 + 1 * pow(2,-45) + 1 * pow(2, -46) + 0 + 0 + 1 * pow(2, -49) + 1 * pow(2, -50) + 0 + 1 *pow(2, -52) + 0 + 0, 52));
好了,干貨分享完畢,謝謝大家!
