P粉0418569552023-08-22 13:16:54
我認為我應該從硬體設計師的角度來添加一些觀點,因為我設計和建立浮點硬體。了解錯誤的起源可能有助於理解軟體中發生的情況,最終,我希望這有助於解釋浮點錯誤發生的原因,並且似乎隨著時間的推移而累積。
從工程角度來看,由於執行浮點計算的硬體只需要在最後一位的一半以下具有一個單位的誤差,所以大多數浮點運算都會有一定的誤差。因此,大多數硬體只會停止在只需要產生一個單操作中最後一位的一半以下誤差的精度,這在浮點除法中尤其棘手。什麼構成一個單一操作取決於單元接受的操作數數量。對於大多數單元來說,是兩個,但有些單元接受3個或更多的操作數。因此,無法保證重複的操作會產生理想的誤差,因為這些誤差會隨著時間的推移而累積。
大多數處理器遵循IEEE-754標準,但有些使用非規範化或不同的標準。例如,IEEE-754中有一種非規範化模式,允許以犧牲精度的方式表示非常小的浮點數。然而,以下將介紹IEEE-754的規範化模式,這是典型的操作模式。
在IEEE-754標準中,硬體設計師可以選擇任何誤差/epsilon值,只要它小於最後一位的一半以下單位,並且結果只需要在一個操作中小於最後一位的一半以下單位。這解釋了為什麼在重複的操作中,誤差會累積。對於IEEE-754雙精確度來說,這是第54位,因為有53位元用來表示浮點數的數值部分(規範化部分),也稱為尾數(例如5.3e5中的5.3)。接下來的幾節將更詳細地介紹各種浮點運算中硬體錯誤的原因。
浮點除法中誤差的主要原因是用來計算商的除法演算法。大多數電腦系統使用乘法的逆來計算除法,主要是在Z=X/Y
和Z = X * (1/Y)
。除法是迭代計算的,即每個週期計算一些商的位數,直到達到所需的精度,對於IEEE-754來說,這是任何誤差小於最後一位的一半以下的內容。 Y的倒數表(1/Y)稱為慢除法中的商選擇表(QST),商選擇表的位數通常是基數的寬度,或每次迭代計算的商的位數加上幾個保護位。對於IEEE-754標準的雙精度(64位元),它將是除法器的基數大小加上幾個保護位元k,其中k>=2
。因此,例如,一個每次計算2位商(基數為4)的典型商選擇表將是2 2= 4
位元(加上幾個可選位元)。
3.1 除法捨入誤差:倒數的近似
商選擇表中的倒數取決於除法方法:慢除法(如SRT除法)或快速除法(如Goldschmidt除法);每個條目根據除法演算法進行修改,以盡量減少誤差。無論如何,所有倒數都是實際倒數的近似,並引入一定的誤差。慢除法和快速除法方法都是迭代計算商,也就是每個步驟計算一定數量的商位數,然後從被除數中減去結果,除數重複這些步驟,直到誤差小於最後一位的一半以下。慢除法方法在每個步驟中計算一定數量的商位數,通常更便宜,而快速除法方法在每個步驟中計算可變數量的位數,通常更昂貴。除法方法最重要的部分是它們大多數都依賴於透過近似的倒數進行重複乘法,因此它們容易出錯。
所有操作中捨入誤差的另一個原因是IEEE-754允許的不同截斷模式。有截斷、向零舍入、向最近舍入(預設)、向下舍入和向上舍入。所有方法都會為單一操作引入小於最後一位的一半單位的誤差。隨著時間和重複操作的進行,截斷也會累積到結果誤差中。這種截斷誤差在涉及某種形式的重複乘法的指數運算中尤其棘手。
由於執行浮點計算的硬體只需要在單一操作中產生一個單位的最後一位的一半以下誤差的結果,如果不加以觀察,隨著重複操作,誤差將會增加。這就是為什麼在需要有界誤差的計算中,數學家使用方法,例如使用IEEE-754的最近舍入的偶數位,因為隨著時間的推移,錯誤更有可能相互抵消,以及區間算術結合變種的IEEE 754舍入模式來預測舍入誤差並進行修正。由於相對誤差較低,與其他舍入模式相比,最近捨入的偶數位元(在最後一位)是IEEE-754的預設舍入模式。
請注意,預設舍入模式,最近捨入的偶數位,保證一個操作的誤差小於最後一位的一半單位。僅使用截斷、向上捨入和向下舍入可能導致大於最後一位的一半單位但小於最後一位單位的誤差,因此除非在區間算
P粉0418819242023-08-22 11:54:59
二進位浮點數運算就是這樣的。在大多數程式語言中,它是基於IEEE 754標準。問題的關鍵在於,數字在這種格式中表示為一個整數乘以二的冪;分母不是二的冪的有理數(例如0.1
,即1/10
)無法被精確表示。
對於標準的binary64
格式中的0.1
,其表示可以精確地寫為
0.1000000000000000055511151231257827021181583404541015625
(十進位),或0x1.999999999999ap-4
(C99十六進位浮點表示法)。 相較之下,有理數0.1
,即1/10
,可以精確地寫成
0.1
(十進位),或0x1.99999999999999...p-4
(類似C99十六進位浮點表示法,其中...
表示無盡的9的序列)。 您程式中的常數0.2
和0.3
也將是它們真實值的近似值。恰好0.2
最接近的double
大於有理數0.2
,但最接近的double
小於有理數0.3
。 0.1
和0.2
的和最終大於有理數0.3
,因此與程式碼中的常數不一致。
關於浮點數運算問題的一個相當全面的處理是《計算機科學家應該了解的浮點運算知識》#。更容易理解的解釋,請參閱floating-point-gui.de。
普通的十進制(基數10)數字也存在相同的問題,這就是為什麼像1/3這樣的數字最終會變成0.333333333...的原因。
你剛好遇到了一個(3/10)在十進制系統中很容易表示,但在二進制系統中無法表示的數字。這種情況也是相反的(在某種程度上):1/16在十進制中是一個醜陋的數字(0.0625),但在二進制中看起來像十進制中的1/10000那樣整潔(0.0001)** -如果我們習慣在日常生活中使用二進制數字系統,你甚至會看到那個數字並本能地理解你可以透過不斷折半來得到它。
當然,這並不完全是浮點數在記憶體中儲存的方式(它們使用一種科學計數法的形式)。然而,它確實說明了一個問題,即二進制浮點精度錯誤往往會出現,因為我們通常感興趣的“現實世界”數字往往是十的冪次數 - 但只是因為我們日常使用的是十進制數係統。這也是為什麼我們會說71%而不是「每7個中的5個」(71%是一個近似值,因為任何十進制數都無法精確地表示5/7)。
所以不,二進制浮點數並沒有損壞,它們只是像其他任何基數N的數系統一樣不完美 :)
在實務上,這種精確度問題意味著您需要使用舍入函數將浮點數舍入到您感興趣的小數位數之前再顯示。
您還需要用允許一定容差的比較來取代等式測試,這表示:
不要使用if (x == y) { ... }
而是用if (abs(x - y) < myToleranceValue) { ... }
。
其中abs
是絕對值函數。需要根據您的特定應用選擇myToleranceValue
- 這與您準備允許多少「擺動空間」以及您要比較的最大數字有很大關係(由於精確度損失問題)。請注意您所選的語言中的“epsilon”樣式常數。這些常數可以用作容差值,但其有效性取決於您正在使用的數字的大小(由於大數計算可能超過epsilon閾值)。