www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]問題背景 背景就簡單點兒說,當初一個項目 C# 編寫,涉及浮點運算,來龍去脈省去,直接看如下代碼。(為什么有這個問題產(chǎn)生,是因為當初線上產(chǎn)生了很詭異的問題,和本地調(diào)試效果不一致。) float?p3x = 80838.0f; float?p2y = -2499.0f; double?v321 = p3x *


問題背景

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果


背景就簡單點兒說,當初一個項目 C# 編寫,涉及浮點運算,來龍去脈省去,直接看如下代碼。(為什么有這個問題產(chǎn)生,是因為當初線上產(chǎn)生了很詭異的問題,和本地調(diào)試效果不一致。)

float p3x = 80838.0f;
float p2y = -2499.0f;
double v321 = p3x * p2y;
Console.WriteLine(v321);


很簡單吧,馬上筆算下結(jié)果為 -202014162,沒問題,難道C#沒有產(chǎn)生這樣的結(jié)果?不可能吧,開啟 VisualStudio,copy代碼試試,果然結(jié)果是-202014162。就這樣完了么?顯然沒有!把編譯時的選項從AnyCPU改成x64試試~(服務(wù)器環(huán)境正是64位滴哦!!)結(jié)果居然變成了-202014160,對沒錯,就是-202014160。細想一下,因為浮點運算的誤差,-202014160 這個結(jié)果是合理的。嗯,再試試C++。// 測試環(huán)境Intel(R) i7-3770 CPU, windows OS 64. Visual Studio 2012 默認設(shè)置。


float p3x = 80838.0f;
float p2y = -2499.0f;
double v321 = p3x * p2y;
std::cout.precision(15);
std::cout << v321 << std::endl;


呃,好像x86、x64都是這個合理的結(jié)果 -202014160。奇了個怪了。其實上面這段C++代碼在不同的平臺下的結(jié)果如下:


  • Windows 32/64位下:-202014160

  • Linux 64位下(CentOS 6 gcc 4.4.7):-202014160

  • Linux 32位下(Ubuntu 12.04+ gcc 4.6.3)是:-202014162

補充說明:當初這篇文章投稿到酷殼,著名程序員左耳朵耗子那邊,這部分結(jié)果數(shù)據(jù)來自耗子叔對文章做的部分調(diào)整。(因為當初行文沒抓住重點,還引來了不少吐槽)

合理的運算結(jié)果,應(yīng)該是-202014160,正確的運算結(jié)果是-202014162,合理性是浮點精度不夠造成的(后文解釋了合理性)。若是用兩個double相乘可得正確且合理的運算結(jié)果。// 就別糾結(jié)我用的“正確、合理”這兩個詞是否恰當了。問題是為何C#下X64和X86結(jié)果不一致?


浮點運算結(jié)果錯誤但合理的解釋

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果


為何  80838.0f * -2499.0f = -202014160.0 是合理的?

32位浮點數(shù)在計算機中的表示方式為:1位符號位(s)-8位指數(shù)位(E)-23位有效數(shù)字(M),即:

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果

其中E是實際轉(zhuǎn)換成1.xxxxx*2^E的指數(shù),M是去掉 1 后的前面的xxxxx(節(jié)約1位)。



1.  80838.0 如何表達?

80838.0 = 1 0011 1011 1100 0110.0(二進制) = 1.0011 1011 1100 0110 0*2^16

有效位M = 0011 1011 1100 0110 0000 000(一共 23 位)

指數(shù)位E = 16 + 127 = 143 = 10001111

內(nèi)部表示 80838.0 = 0 [10001111] [0011 1011 1100 0110 0000 000] = 0100 0111 1001 1101 1110 0011 0000 0000 = 47 9d e3 00 //實際調(diào)試時看到的內(nèi)存值 可能是00 e3 9d 47是因為調(diào)試環(huán)境用了小端表示法法:低位字節(jié)排內(nèi)存低地址端,高位排內(nèi)存高地址

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果



2. -2499.0 如何表達?


-2499.0 = -100111000011.0 = -1.001110000110 * 2^11

有效位M = 0011 1000 0110 0000 0000 000

指數(shù)位E = 11+127=138= 10001010

符號位s = 1

內(nèi)部表示-2499.0 = 1 [10001010] [0011 1000 0110 0000 0000 000]

=1100 0101 0001 1100 0011 0000 0000 0000 =c5 1c 30 00

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果



3. 如何計算 80838.0 * -2499.0 = ?



指數(shù) e = 11+16 = 27

則指數(shù)位 E = e + 127 = 154 = 10011010

有效位相乘結(jié)果為 1.1000 0001 0100 1111 1011 1010 01 (可以自己動手實際算下),實際中只能有23位,后面的被截斷即1000 0001 0100 1111 1011 1010 01,相乘結(jié)果內(nèi)部表示=1[10011010][1000 0001 0100 1111 1011 101= 1100 1101 0100 0000 1010 0111 1101 1101 = cd 40 a7 dd

結(jié)果 = -1.1000 0001 0100 1111 1011 101 *2^27

= -11000 0001 0100 1111 1011 1010000

= -202014160

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果


通過上面得知,32 位浮點數(shù),-202014160 就是合理的結(jié)果,完全能解釋清楚。但如果有效數(shù)字更長的話, 上面的就不會被截斷。



4. 正確的結(jié)果-202014162怎么得來?


有效位相乘結(jié)果為 1.1000 0001 0100 1111 1011 1010 01

即結(jié)果 = -1.1000 0001 0100 1111 1011 101001 *2^27

= -11000 0001 0100 1111 1011 101001 = -202014162


一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果



根因挖掘

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果


上面部分解釋了兩種結(jié)果的來源,但貌似沒從根本回到為什么?用C++同樣的代碼,X86,X64(DEBUG下,這個后面會說)下得到一致的結(jié)果-202014160,容易理解且也是合理的。原因何在?看下編譯后生成的代碼(截取關(guān)鍵部分)

//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結(jié)果一致)即都用了先用浮點乘起來(mulss),然后轉(zhuǎn)成double(cvtss2sd)。從上面的匯編代碼可以看出 C# X86生成代碼用的指令fld/fmul/fstp等。其中fld/fmul/fstp等指令是由FPU(float point unit)浮點運算處理器做的,F(xiàn)PU在進行浮點運算時,用了80位的寄存器做相關(guān)浮點運算,然后再根據(jù)是float/double截取成32位或64位。非FPU的情況是用了SSE中128位寄存器(float實際只用了其中的32位,計算時也是以32位計算的),這就是導(dǎo)致上述問題產(chǎn)生的最終原因。


浮點運算標準IEEE-754 推薦標準實現(xiàn)者提供浮點可擴展精度格式(Extended precision),Intel x86處理器有FPU(float point unit)浮點運算處理器支持這種擴展。C#的浮點是支持該標準的,其中其官方文檔也提到了浮點運算可能會產(chǎn)生比返回類型更高精度的值(正如上面的返回值精度就超過了float的精度),并說明如果硬件支持可擴展浮點精度的話,那么所有的浮點運算都將用此精度進行以提高效率,舉個例子x*y/z, x*y的值可能都在double的能力范圍之外了,但真實情況可能除以z后又能把結(jié)果拉回到double范圍內(nèi),這樣的話,用了FPU的結(jié)果就會得到一個準確的double值,而非FPU的就是無窮大之類的了。


即產(chǎn)生如上的結(jié)果原因是,兩個浮點數(shù)相乘在非FPU的情況下,用了32位計算產(chǎn)生的結(jié)果導(dǎo)致結(jié)果存在誤差,而FPU是用了80位進行計算的,所以得到的結(jié)果是精度很高的,體現(xiàn)在本文的案例上就是個位數(shù)上的2。所以大家在寫代碼的時候得保證實際運行環(huán)境/測試環(huán)境/開發(fā)環(huán)境的一致性(包括OS架構(gòu)啊、編譯選項等)啊,不然莫名其妙的問題會產(chǎn)生(本文就是開發(fā)環(huán)境與運行環(huán)境不一致導(dǎo)致的問題,糾結(jié)了好久才發(fā)現(xiàn)是這個原因);遇到涉及浮點運算的時候別忘了有可能是這個原因產(chǎn)生的;另外,float/double混用的情況得特別注意。


總結(jié)一下,本文通過分析之前遇到的一個疑難雜癥帶著大家一塊回顧或者學(xué)習(xí)了一下計算機內(nèi)部浮點數(shù)的表達,解決了疑問。時候可能需要跟進到硬件底層,當然隨著術(shù)的發(fā)展,可能理所當然的東西在新硬件的情況下也會有所不同(例如文中提到的 FPU 也有更高端的技術(shù)來替換了,本人對于硬件這塊了解不多,感興趣可以查閱更多材料,閱讀原文有更多參考資料)。

特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果

長按訂閱更多精彩▼

一個由跨平臺產(chǎn)生的浮點數(shù)bug | 有你意想不到的結(jié)果

如有收獲,點個在看,誠摯感謝

免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
關(guān)閉
關(guān)閉