上一章我們簡(jiǎn)單介紹了IEEE浮點(diǎn)標(biāo)準(zhǔn),本次我們主要講解一下浮點(diǎn)運(yùn)算舍入的問題,,以及簡(jiǎn)單的介紹浮點(diǎn)數(shù)的運(yùn)算。
之前我們已經(jīng)提到過,,有很多小數(shù)是二進(jìn)制浮點(diǎn)數(shù)無法準(zhǔn)確表示的,,因此就難免會(huì)遇到舍入的問題。這一點(diǎn)其實(shí)在我們平時(shí)的計(jì)算當(dāng)中會(huì)經(jīng)常出現(xiàn),,就比如之前我們提到過的0.3,,它就是無法用浮點(diǎn)小數(shù)準(zhǔn)確表示的。
為此LZ專門寫了一個(gè)小程序,,使用Java語言打印出了0.3的二進(jìn)制表示,,是這樣的一個(gè)數(shù)字,0 01111101 00110011001100110011010,。我們來簡(jiǎn)單算一下,,這個(gè)數(shù)值大約是多少。它的階碼在偏置之后的值為-2,,它的尾數(shù)位在加1之后為1 + 1/8 + 1/16 + 1/128 + 1/256 = 1.19921875,。后面還有有效位,不過我們只大概計(jì)算一下,,就不算那么精確了,,最終算出來的值為0.2998046875。(LZ用計(jì)算器算的,,0.0)
可以看出,,這個(gè)值離0.3已經(jīng)非常接近了,而且我們還省略了一小部分有效小數(shù)位,,但是不管怎么說,,二進(jìn)制無法像十進(jìn)制小數(shù)一樣,準(zhǔn)確的表示0.3這個(gè)數(shù)值,。因此舍入這一部分是浮點(diǎn)數(shù)無法逃脫的內(nèi)容,。
浮點(diǎn)數(shù)舍入
在我們平時(shí)日常使用的十進(jìn)制當(dāng)中,我們一般對(duì)一個(gè)無理數(shù)或者有位數(shù)限制的有理數(shù)進(jìn)行舍入時(shí),,大部分時(shí)候會(huì)采取四舍五入的方式,,這算是一種比較符合我們期望的舍入方式。
不過針對(duì)浮點(diǎn)數(shù)來說,我們的舍入方式會(huì)更豐富一些,。一共有四種方式,,分別是向偶數(shù)舍入,、向零舍入,、向上舍入以及向下舍入。
這四種舍入方式都不難理解,,其中向偶數(shù)舍入就是向最靠近的偶數(shù)舍入,,比如將1.5舍入為2,將0.1舍入為0,。而向零舍入則是向靠近零的值舍入,,比如將1.5舍入為1,將0.1舍入為0,。對(duì)于向上舍入來說,,則是往大了(也就是向正無窮大)舍入的意思,比如將1.5舍入為2,,將-1.5舍入為-1,。而向下舍入則與向上舍入相反,是向較小的值(也就是向負(fù)無窮大)舍入的意思,。
這里需要提一下的是,,除了向偶數(shù)舍入以外,其它三種方式都會(huì)有明確的邊界,。這里的含義是指這三種方式舍入后的值x'與舍入之前的值x會(huì)有一個(gè)明確的大小關(guān)系,,比如對(duì)于向上舍入來說,則一定有x <= x',。對(duì)于向零舍入來說,,則一定有|x| >= |x'|。
對(duì)于向偶數(shù)舍入來講,,它最大的作用是在統(tǒng)計(jì)時(shí)使用,。向偶數(shù)舍入可以讓我們?cè)诮y(tǒng)計(jì)時(shí),將舍入產(chǎn)生的誤差平均,,從而盡可能的抵消,。而其它三種方式在這方面都是有一定缺陷的,向上和向下舍入很明顯,,會(huì)造成值的偏大或偏小,。而對(duì)于向零舍入來講,如果全是正數(shù)的時(shí)候則會(huì)造成結(jié)果偏小,,全是負(fù)數(shù)的時(shí)候則會(huì)造成結(jié)果偏大,。
通常情況下我們采取的舍入規(guī)則是在原來的值是舍入值的中間值時(shí),采取向偶數(shù)舍入,在二進(jìn)制中,,偶數(shù)我們認(rèn)為是末尾為0的數(shù),。而倘若不是這種情況的話,則一般會(huì)有選擇性的使用向上和向下舍入,,但總是會(huì)向最接近的值舍入,。其實(shí)這正是IEEE采取的默認(rèn)的舍入方式,因?yàn)檫@種舍入方式總是企圖向最近的值的舍入,。
比如對(duì)于10.10011這個(gè)值來講,,當(dāng)舍入到個(gè)位數(shù)時(shí),會(huì)采取向上舍入,,因此此時(shí)的值為11,。當(dāng)舍入到小數(shù)點(diǎn)后1位時(shí),會(huì)采取向下舍入,,因此此時(shí)的值為10.1,。當(dāng)舍入到小數(shù)點(diǎn)后4位時(shí),由于此時(shí)為10.10011舍入值的中間值,,因此采用向偶數(shù)舍入,,此時(shí)舍入后的值為10.1010?! ?/p>
Java當(dāng)中的浮點(diǎn)數(shù)舍入
之前我們講解了一堆舍入的方式,,最終我們給出一個(gè)結(jié)論,就是IEEE標(biāo)準(zhǔn)默認(rèn)的舍入方式,,是企圖向最近的值舍入(Round to the Nearest Value),。
上面我們已經(jīng)詳細(xì)的解釋了IEEE標(biāo)準(zhǔn)中默認(rèn)的舍入方式(黑色加粗的那部分解釋),但是估計(jì)還是會(huì)有不少猿友比較迷糊,,書中也沒有給出具體的例子,,因此這里L(fēng)Z以Java語言為例,我們直接寫程序來看一下,,看看Java當(dāng)中的舍入方式是否是按照我們所說的進(jìn)行的,。
在各位看這個(gè)測(cè)試程序之前,LZ需要再給各位再解釋一下中間值的概念,。中間值就是指的,,比如1.1(二進(jìn)制)這個(gè)數(shù)字,假設(shè)要舍入到個(gè)位,,那么它就是一個(gè)中間值,,因?yàn)樗幱?(二進(jìn)制)和10(二進(jìn)制)的中間,在這個(gè)時(shí)候?qū)?huì)采用向偶數(shù)舍入的方式,。
下面便是LZ寫的測(cè)試程序,,其中那些具體的浮點(diǎn)數(shù)值是使用二進(jìn)制小數(shù)的算法計(jì)算出來的,,各位猿友不必在意,如果你不嫌麻煩,,也可以自己手算一下,。我們主要看的是最終的舍入情況。
public class Main{
public static void main(String[] args){
System.out.println("舍入前: 10.10011111111111111111101");
System.out.print("舍入后:");
printFloatBinaryString(2.62499964237213134765625f);
System.out.println();
System.out.println("舍入前: 10.10011111111111111111111");
System.out.print("舍入后:");
printFloatBinaryString(2.62499988079071044921875f);
System.out.println();
System.out.println("舍入前: 10.10011111111111111111101011");
System.out.print("舍入后:");
printFloatBinaryString(2.62499968707561492919921875f);
System.out.println();
System.out.println("舍入前: 10.10011111111111111111100011");
System.out.print("舍入后:");
printFloatBinaryString(2.62499956786632537841796875f);
System.out.println();
System.out.println("舍入前: -10.10011111111111111111101");
System.out.print("舍入后:");
printFloatBinaryString(-2.62499964237213134765625f);
System.out.println();
System.out.println("舍入前: -10.10011111111111111111111");
System.out.print("舍入后:");
printFloatBinaryString(-2.62499988079071044921875f);
System.out.println();
System.out.println("舍入前: -10.10011111111111111111101011");
System.out.print("舍入后:");
printFloatBinaryString(-2.62499968707561492919921875f);
System.out.println();
System.out.println("舍入前: -10.10011111111111111111100011");
System.out.print("舍入后:");
printFloatBinaryString(-2.62499956786632537841796875f);
System.out.println();
}
public static void printFloatBinaryString(Float f){
char[] binaryChars = getBinaryChars(f);
for (int i = 0; i < binaryChars.length; i++) {
System.out.print(binaryChars[i]);
if (i == 0 || i == 8) {
System.out.print(" ");
}
}
System.out.println();
}
public static char[] getBinaryChars(Float f){
char[] result = new char[32];
char[] binaryChars = Integer.toBinaryString(Float.floatToIntBits(f)).toCharArray();
if (binaryChars.length < result.length) {
System.arraycopy(binaryChars, 0, result, result.length - binaryChars.length, binaryChars.length);
for (int i = 0; i < result.length - binaryChars.length; i++) {
result[i] = '0';
}
}else {
result = binaryChars;
}
return result;
}
}
上面是測(cè)試程序,,其實(shí)程序中看不出什么,,就是一堆輸出語句。如果各位猿友有興趣,,也可以簡(jiǎn)單看一下程序的實(shí)現(xiàn),。不過我們主要還是看結(jié)果。
上面一共有8次舍入,,前4次是正數(shù),后4次是負(fù)數(shù),??梢钥闯鰧?duì)于正負(fù)數(shù)來講,舍入后的位表示是一樣的,,只是最高位的符號(hào)位不同而已,,因此這里L(fēng)Z就不再分析下面4個(gè)負(fù)數(shù)的舍入方式了,我們主要來看前4次舍入,。
第1次和第2次對(duì)于末尾01和11的舍入,,由于是中間值,因此全部采取的向偶數(shù)舍入的方式,,保證最低位為0,。第3次由于比中間值大,而數(shù)值又是正數(shù),,因此采用向上舍入的方式,。第4次則比中間值小,數(shù)值也同樣是正數(shù),,因此采用向下舍入的方式,。
由此可以看出,Java正是采用的我們所描述的方式進(jìn)行舍入操作的,,也就是總是企圖朝最近的數(shù)值舍入,。相對(duì)于其它語言,由于LZ主修Java,,例子篇幅也比較長(zhǎng),,因此這里就不寫其他語言的例子了,有興趣的猿友可以嘗試寫一下C/C++或者C#的例子來看一下,,看是否是采用的同樣的舍入方式,。
浮點(diǎn)數(shù)運(yùn)算
在IEEE標(biāo)準(zhǔn)中,,制定了關(guān)于浮點(diǎn)數(shù)的運(yùn)算規(guī)則,就是我們將把兩個(gè)浮點(diǎn)數(shù)運(yùn)算后的精確結(jié)果的舍入值,,作為我們最終的運(yùn)算結(jié)果,。正是因?yàn)橛辛诉@一個(gè)特殊點(diǎn),就會(huì)造成浮點(diǎn)數(shù)當(dāng)中,,很多運(yùn)算不滿足我們平時(shí)熟知的一些運(yùn)算特性,。
比如加法的結(jié)合律,也就是a + b + c = a + (b + c),,這是很普通的加法運(yùn)算的特性,,但是浮點(diǎn)數(shù)是不滿這一特性的,比如說下面這一段小程序,。
public static void main(String[] args){
System.out.println(1f + 10000000000f - 10000000000f);
System.out.println(1f + (10000000000f - 10000000000f));
}
這一段程序會(huì)依次輸出0.0和1.0,,正是因?yàn)樯崛攵斐傻倪@一誤差。在第一個(gè)輸出語句中,,計(jì)算1f+10000000000f時(shí),,會(huì)將1這個(gè)有效數(shù)值舍入掉,而導(dǎo)致最終結(jié)果為0.0,。而在第二個(gè)輸出語句中10000000000f-10000000000f將先得到結(jié)果0.0,,因此最終的結(jié)果為1.0。
相應(yīng)的,,浮點(diǎn)數(shù)運(yùn)算對(duì)乘法也不滿足結(jié)合律,,也就是 a * b * c != a * (b * c),同時(shí)也不滿足分配律,,即 a * (b + c) != a * b + a * c,。
浮點(diǎn)數(shù)失去了很多運(yùn)算方面的特性,因此也導(dǎo)致很多優(yōu)化手段無法進(jìn)行,,比如我們?cè)噲D優(yōu)化下面這樣一段程序,。
/* 優(yōu)化前 */
float x = a + b + c;
float y = b + c + d;
/* 優(yōu)化后 */
float t = b + c;
float x = a + t;
float y = t + d;
對(duì)于優(yōu)化前的代碼來講,進(jìn)行了4次浮點(diǎn)運(yùn)算,,而優(yōu)化后則是3次,。然而這種優(yōu)化是編譯器無法進(jìn)行的,因?yàn)榭赡軙?huì)引入誤差,,比如就像前面的小例子中的結(jié)果0和1一樣,。編譯器在此時(shí)一般是不敢進(jìn)行優(yōu)化的,試想一下,,如果是銀行系統(tǒng)的匯款或者收款等功能,,如果編譯器進(jìn)行優(yōu)化的話,很可能一不小心就把別人的錢給優(yōu)化掉了,。
文章小結(jié)
2.X系列主要講解了二進(jìn)制的位表示方式,、無符號(hào)以及補(bǔ)碼編碼以及二進(jìn)制整數(shù)和浮點(diǎn)數(shù)的表示方式和運(yùn)算,。這一章是2.X的最后一章,下一章我們將進(jìn)入匯編語言3.X的世界,,那里我們可以看到程序是如何使用寄存器和存儲(chǔ)器的,、如何表示C語言中的指針、匯編語言如何實(shí)現(xiàn)程序的流程控制等等一系列內(nèi)容,。相對(duì)來講,,3.X的內(nèi)容會(huì)比2.X的內(nèi)容有意思很多,因此希望各位猿友不要錯(cuò)過,。