Re: [問題] float 和 double 的問題
引文做 high-light。
可能要講的太多了,也不知道你對於 IEEE754 了解到哪種程度,所以大多人也不知該從
何講起,唯這問題似乎真的卡很久,只丟一點小弟知道的東西討論。
但是我看有些網頁用IEEE745的表示法下去算..
範圍卻是1.2E-38~3.4E+38 不知道那個是正確的呢??
這個是正確的,詳細的推導在中文的 wiki 介紹就很清楚了,但尷尬的是浮點數表示法
不一定從 IEEE754 規格,即使從 IEEE754 規格,不同編譯器細節上的操作也可能會有
所不同,如果我沒記錯的話,gcc 3.4.1 (就是你 dev-c++ 用的 gcc 版本) 和 vc 在
運算上,及最小值定義上就有所不同。故一般書籍都是建議去查 float.h 之 macro。
float a=0.01;
首先 a 存 0.01 時,在存入 ieee 754 就會有誤差了,單純轉成二進位約是
0.000000 101000111101011100001010 0011110101 ....
正規化後得 1.01000111101011100001010 * 2^-7 ,再存入 IEEE754 欄位中。
但將上面這數值再轉回十進位時,它只能存 23 位(加上小數點前隱性的1是24位),
這種實際上不只23位可表達的十進位,最後必然產生捨位誤差(Lemma),實際上得到
的就已經不是 0.01,捨位誤差後得到的約是
0.0099999997764826
Q1 : 可是書上寫的是「小數後7位」,你上面寫的已經到「16位了耶」
A1 : 所以說了,書上寫的都只是概估值,他們說的「小數點後七位」,算法是
2^(23+1) = 10^x , x = log10(2^24) = 24 * log10(2) = 7.22,但這數
據也只能當參考而已,我們光是拿最後一個 mantissa 位元 2^-23 這位來算,
它實際表示是 0.00000011920928955078125 ,是小數點後 23 位,不就超過小
數點後七位了嗎?
其中2^(23+1),裡面的 +1,正代表著正規化後,小數前唯一的一個1。
Q2 : 可是我用 printf 根本就看不出來它的效果耶?
A2 : 原因是 printf 根本就沒在分 %f, %lf,在 printf 裡面,你拿 double / float
混用於 %f , %lf 結果都是一樣的,會先轉成 double,(強調是 printf )
這種方式根本就看不出結果,所以關於 float/double 混用比較建議自己搞。
[Lemma] 「捨位誤差」有時候會看小數後第 24 位,像是十進位表示法的「四捨五入」
,如果第24位是0就刪掉,如果第24位是1,前23位都加1。這種做調整的方式
不只一種,像無條件進1、無條件捨去等 (所以產生了四捨六入五成雙說法),
但這裡小提一下,就不再深入。
我還是覺得奇怪..如果浮點數存的時候就有誤差(因為只有23位可存尾數)..
所以存在電腦裡應該0.01是存成0.00999999....
那在printf("%f",0.01)的時候..應該就會印成0.00999999
原因是預設列印是輸出小數後 6 位數進行四捨五入,然後答案是 0.009999999...
自然就變 0.01。
這樣乘下來怎麼去看它的結果精確度到幾位?
還有我參考了很多文章..都說乘法時並不是用float下去算是用double
儲存再放到float裡..又更複雜了..
這個我記得有可能是這樣情況沒錯,這問題實質上相依於 compiler 較多,確實
有些 compiler 在 float 運算時會先偷轉 double 再放到 float,如 BCB 就這
樣,原因是對於現代 GPU 而言(扣除掉叫顯卡做這運算),double 除了精度高,
速度也比 float 還快,(所以一般 C++ 調用 double 系列之 math library ,
有些情況下比調用 float 系列之 library 速度來得快),其他部份沒再特別確認。
3535=110111001111
float 0.01*3 =1.0100011110101110*2^-7 X 1.01*2^2 (小數點後2位)
*3535= X 1.10111001111*2^11(小數點後11位)
(0) a = 0.01 (dec) = 1.01000111101011100001010 * 2^-7
b = 3535 (dec) = 1.10111001111000000000000 * 2^11
(1) mantissa 先相乘
1.01000111101011100001010(bin) * 1.10111001111000000000000(bin)
= 10.00110101100110011001100 1100010110000000000000 (bin)
上面這結果用手算會算到爆,建議直接先去找大數函式庫下來做
< 我是之前自己做過一份 binary 就是了 >
(2) 指數相加 , -7 + 11 = 4
(3) 正規化, c = a * b = 10.00110101100110011001100 * 2^4
= 1.00011010110011001100110 * 2^5
這個數字轉到 10 進位約是 35.3499984741210940(誤差約1.5e-6) ,
但誤差是從 0.01 開始就存在的,所以導致不如所預期的誤差在 1E-7 以下,
原因都是在於真正運算,必須考量實際是以二進位方式做運算,且會有捨位誤差
現象之存在。
再來是看這 sample。
int main()
{
float x = 0.01;
float y = x * 3535;
printf("%.16f\n", x * 3535);
printf("%.16f\n", y);
return 0;
}
兩個答案都是 35.349984741120940,但如果你使用了 %.2f,或%.3f 時,
這裡的「顯示」會是小數後 2/3 位四捨五入,達成「看似無誤差」的 35.350,
但真正數值還是要以二進位方式,深入欄位格式做分析才準!
有人知道怎麼看乘法的精確度嗎??
如果你期望的是,希望能正確在某次運算後,得到答案的 正確誤差值,那我只能說
很遺憾的,基本上辦不到,或很難!原因在於一般 user / coder 在填寫十進位字面
常量時,這時候就會有捨位誤差存在,甚至有其他更多原因可導致誤差,但說完大概
也出了一本書了。而誤差是多少?不一定!要看轉到欄位裡才知道,有些是 10^0 級
,有些是 2^-n 級, 更有些是 2^n 級,都不一定。
我沒記錯的話,之前自修看計算機組織與結構(白算盤),裡面有個章節都有討論 IEEE754
,而且還蠻詳細的,包含 + - * / 都有流程圖出來,裡面的 K 完會蠻有幫助的。
---
基本上這篇問題該回答的都答完了,如果有什麼其他不懂、疑惑的地方,
或許再以二進位方式做欄位分析,會得到更多答案。
其他比較 funny 的議題,像是小算盤按上 1/3 得到 0.33333333...,結果再 *3 ,
為什麼可以再回到 1 之類的 (有些計算機只能到 0.9999999),這種議題就留下來做
思考,不再贅述。
--
~ 這輩子與神手無緣
我只好當神獸了 ~
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 180.177.76.161
※ 編輯: EdisonX 來自: 180.177.76.161 (01/17 20:29)
拋磚引玉。
推 hateexam:推 好文~ 01/18 00:38
推 sunlights:好厲害!!! 01/18 01:25
推 VictorTom:推; 另外以前也看過直接暫放浮點暫存器(80b), 比64b的 01/18 03:15
→ VictorTom:double精度更高; 以前被這種optimize造成test fail過Orz 01/18 03:16
→ EdisonX:V 大沒講我都忘了這件事,確實有種說法是若 long double 01/18 03:28
→ EdisonX:吃 80 bits,速度和精度會比 double 快,所以會用 80b 算, 01/18 03:29
→ EdisonX:不過一直沒機會驗證, 謝謝 V 大補充提點 :) 01/18 03:29
→ azureblaze:x86的fpu只做了80bit的,所以float和double都直接轉80 01/18 09:42
→ azureblaze:算完放回記憶體的時候再把多的位數砍掉 01/18 09:43
→ azureblaze:因為效率之類的問題很多人都不鳥IEEE 01/18 09:47
→ azureblaze:所以跨平台浮點運算要得到一樣的結果超麻煩 01/18 09:47
→ azureblaze:(做連線遊戲、replay之類的) 01/18 09:48
→ azureblaze:光是intel和amd結果就會不同了 01/18 09:51
→ EdisonX:原來如此,感謝 a 大補充。 01/18 14:38
推 amozartea:學到了..原來有時call double會比float快... 01/19 02:15
推 hilorrk:新版 QEMU 為了解決這問題把所有浮點運算用軟體模擬... 01/19 23:04
推文自動更新已關閉
留言