碼農唐磊 程序猿石頭
程序員
本文爲 6 年前的舊文整理重發,由於最開始是 workdpress 的程序,後改成靜態 blog 過程當中,致使格式等混亂,這篇年久失修舊文可文末點擊原文訪問。windows
背景就簡單點兒說,當初一個項目 C# 編寫,涉及浮點運算,前因後果省去,直接看以下代碼。(爲何有這個問題產生,是由於當初線上產生了很詭異的問題,和本地調試效果不一致。)服務器
float p3x = 80838.0f; float p2y = -2499.0f; double v321 = p3x * p2y; Console.WriteLine(v321);
很簡單吧,立刻筆算下結果爲 -202014162,沒問題,難道C#沒有產生這樣的結果?不可能吧,開啓 VisualStudio,copy代碼試試,果真結果是-202014162。就這樣完了麼?顯然沒有!把編譯時的選項從AnyCPU改爲x64試試~(服務器環境正是64位滴哦!!)結果竟然變成了-202014160,對沒錯,就是-202014160。細想一下,由於浮點運算的偏差,-202014160 這個結果是合理的。嗯,再試試C++。// 測試環境Intel(R) i7-3770 CPU, windows OS 64. Visual Studio 2012 默認設置。架構
float p3x = 80838.0f; float p2y = -2499.0f; double v321 = p3x * p2y; std::cout.precision(15); std::cout << v321 << std::endl;
呃,好像x8六、x64都是這個合理的結果 -202014160。奇了個怪了。其實上面這段C++代碼在不一樣的平臺下的結果以下:ide
補充說明:當初這篇文章投稿到酷殼,著名程序員左耳朵耗子那邊,這部分結果數據來自耗子叔對文章作的部分調整。(由於當初行文沒抓住重點,還引來了很多吐槽)
合理的運算結果,應該是-202014160,正確的運算結果是-202014162,合理性是浮點精度不夠形成的(後文解釋了合理性)。如果用兩個double相乘可得正確且合理的運算結果。// 就別糾結我用的「正確、合理」這兩個詞是否恰當了。問題是爲什麼C#下X64和X86結果不一致?學習
爲什麼 80838.0f * -2499.0f = -202014160.0 是合理的?測試
32位浮點數在計算機中的表示方式爲:1位符號位(s)-8位指數位(E)-23位有效數字(M),即:
其中E是實際轉換成1.xxxxx*2^E的指數,M是去掉 1 後的前面的xxxxx(節約1位)。調試
80838.0 如何表達? 0 = 1 0011 1011 1100 0110.0(二進制) = 1.0011 1011 1100 0110 0*2^16 有效位M = 0011 1011 1100 0110 0000 000(一共 23 位) 指數位E = 16 + 127 = 143 = 10001111 內部表示 80838.0 = 0 [10001111] [0011 1011 1100 0110 0000 000] = 0100 0111 1001 1101 1110 0011 0000 0000 = 47 9d e3 00 //實際調試時看到的內存值 多是00 e3 9d 47是由於調試環境用了小端表示法法:低位字節排內存低地址端,高位排內存高地址
2. -2499.0 如何表達? -2499.0 = -100111000011.0 = -1.001110000110 * 2^11 有效位M = 0011 1000 0110 0000 0000 000 指數位E = 11+127=138= 10001010 符號位s = 1 內部表示-2499.0 = 1 [10001010] [0011 1000 0110 0000 0000 000] =1100 0101 0001 1100 0011 0000 0000 0000 =c5 1c 30 00
3. 如何計算 80838.0 * -2499.0 = ? 指數 e = 11+16 = 27 則指數位 E = e + 127 = 154 = 10011010 有效位相乘結果爲 1.1000 0001 0100 1111 1011 1010 01 (能夠本身動手實際算下),實際中只能有23位,後面的被截斷即1000 0001 0100 1111 1011 1010 01,相乘結果內部表示=1[10011010][1000 0001 0100 1111 1011 101] = 1100 1101 0100 0000 1010 0111 1101 1101 = cd 40 a7 dd 結果 = -1.1000 0001 0100 1111 1011 101 *2^27 = -11000 0001 0100 1111 1011 1010000 = -202014160
經過上面得知,32 位浮點數,-202014160 就是合理的結果,徹底能解釋清楚。但若是有效數字更長的話, 上面的就不會被截斷。code
4. 正確的結果-202014162怎麼得來? 有效位相乘結果爲 1.1000 0001 0100 1111 1011 1010 01 即結果 = -1.1000 0001 0100 1111 1011 101001 *2^27 = -11000 0001 0100 1111 1011 101001 = -202014162
上面部分解釋了兩種結果的來源,但貌似沒從根本回到爲何?用C++一樣的代碼,X86,X64(DEBUG下,這個後面會說)下獲得一致的結果-202014160,容易理解且也是合理的。緣由何在?看下編譯後生成的代碼(截取關鍵部分)blog
//C# x86 下 ...... float p3x = 80838.0f; 0000003b mov dword ptr [ebp-40h],479DE300h float p2y = -2499.0f; 00000042 mov dword ptr [ebp-44h],0C51C3000h double v321 = p3x * p2y; 00000049 fld dword ptr [ebp-40h] 0000004c fmul dword ptr [ebp-44h] 0000004f fstp qword ptr [ebp-4Ch] ....... //C# X64下 ...... float p3x = 80838.0f; 00000045 movss xmm0,dword ptr [00000098h] 0000004d movss dword ptr [rbp+3Ch],xmm0 float p2y = -2499.0f; 00000052 movss xmm0,dword ptr [000000A0h] 0000005a movss dword ptr [rbp+38h],xmm0 double v321 = p3x * p2y; 0000005f movss xmm0,dword ptr [rbp+38h] 00000064 mulss xmm0,dword ptr [rbp+3Ch] 00000069 cvtss2sd xmm0,xmm0 0000006d movsd mmword ptr [rbp+30h],xmm0 ......
C++ x86 / x64下都生成了相似的代碼(這也就是爲什麼 C++ x86/x64與C#x64結果一致)即都用了先用浮點乘起來(mulss),而後轉成double(cvtss2sd)。從上面的彙編代碼能夠看出 C# X86生成代碼用的指令fld/fmul/fstp等。其中fld/fmul/fstp等指令是由FPU(float point unit)浮點運算處理器作的,FPU在進行浮點運算時,用了80位的寄存器作相關浮點運算,而後再根據是float/double截取成32位或64位。非FPU的狀況是用了SSE中128位寄存器(float實際只用了其中的32位,計算時也是以32位計算的),這就是致使上述問題產生的最終緣由。
浮點運算標準IEEE-754 推薦標準實現者提供浮點可擴展精度格式(Extended precision),Intel x86處理器有FPU(float point unit)浮點運算處理器支持這種擴展。C#的浮點是支持該標準的,其中其官方文檔也提到了浮點運算可能會產生比返回類型更高精度的值(正如上面的返回值精度就超過了float的精度),並說明若是硬件支持可擴展浮點精度的話,那麼全部的浮點運算都將用此精度進行以提升效率,舉個例子xy/z, xy的值可能都在double的能力範圍以外了,但真實狀況可能除以z後又能把結果拉回到double範圍內,這樣的話,用了FPU的結果就會獲得一個準確的double值,而非FPU的就是無窮大之類的了。
即產生如上的結果緣由是,兩個浮點數相乘在非FPU的狀況下,用了32位計算產生的結果致使結果存在偏差,而FPU是用了80位進行計算的,因此獲得的結果是精度很高的,體如今本文的案例上就是個位數上的2。因此你們在寫代碼的時候得保證明際運行環境/測試環境/開發環境的一致性(包括OS架構啊、編譯選項等)啊,否則莫名其妙的問題會產生(本文就是開發環境與運行環境不一致致使的問題,糾結了很久才發現是這個緣由);遇到涉及浮點運算的時候別忘了有多是這個緣由產生的;另外,float/double混用的狀況得特別注意。
總結一下,本文經過分析以前遇到的一個疑難雜症帶着你們一塊回顧或者學習了一下計算機內部浮點數的表達,解決了疑問。有時候可能須要跟進到硬件底層,固然隨着硬件技術的發展,可能之前理所固然的東西在新硬件的狀況下也會有所不一樣(例如文中提到的 FPU 也有更高端的技術來替換了,本人對於硬件這塊瞭解很少,感興趣能夠查閱更多材料,閱讀原文有更多參考資料)。
老規矩,若是有幫助(對你身邊的其餘人有幫助也行呀),寫篇文章不容易,但願親多多幫忙「在看」,轉發分享支持。