考慮以下程式碼:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
為什麼會出現這些不準確的情況?
P粉5205457532023-10-12 10:27:06
我相信我應該添加硬體設計師的觀點,因為我設計和建立浮點硬體。了解錯誤的根源可能有助於理解軟體中發生的情況,最終,我希望這有助於解釋浮點錯誤發生並似乎隨著時間的推移而累積的原因。
從工程角度來看,大多數浮點運算都會存在一些誤差,因為進行浮點計算的硬體只需要在最後一位的誤差小於一個單位的二分之一。因此,許多硬體將停止在一個精確度,該精確度僅需要在單一操作的最後一位產生小於一個單位的誤差,這在浮點除法中尤其成問題。單一操作的構成取決於該單元需要多少個操作數。對於大多數來說,它是兩個,但有些單元需要 3 個或更多操作數。因此,不能保證重複操作會導致所需的錯誤,因為錯誤會隨著時間的推移而累積。
大多數處理器遵循 IEEE-754 標準,但有些處理器使用非規範化或不同的標準 。例如,IEEE-754 中有一種非規範化模式,它允許以犧牲精度為代價來表示非常小的浮點數。不過,以下將介紹 IEEE-754 的標準化模式,這是典型的操作模式。
在IEEE-754 標準中,硬體設計者允許使用任何error/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 的預設舍入模式。
請注意,預設捨入模式,捨去到最近的最後一位的偶數 a>,保證一次運算最後一位的誤差小於二分之一。單獨使用截斷、向上取整、向下取整可能會導致誤差大於最後一位的二分之一,但小於最後一位的一個單位,所以不建議使用這些模式,除非是用於區間算術。
簡而言之,浮點運算出錯的根本原因是硬體截斷和除法時倒數截斷的結合。由於IEEE-754標準只要求單次運算的最後一位的誤差小於二分之一,因此重複運算的浮點誤差將會累加,除非進行修正。
P粉5634465792023-10-12 10:02:37
二進位浮點數學就像這樣。在大多數程式語言中,它是基於 IEEE 754 標準。問題的關鍵在於,數字以此格式表示為整數乘以2 的冪;分母不是2的冪的有理數(例如0.1
,即1/10
)無法精確表示。
對於標準 binary64
格式的 0.1
,其表示形式可以完全寫為
0.10000000000000000055511151231257827021181583404541015625
(十進位),或0x1.999999999999ap-4
C99 十六進位浮點表示法< /a>.#相較之下,有理數0.1
,即1/10
,可以完全寫成
0.1
(十進位),或0x1.99999999999999...p-4
類似於 C99 十六進位浮點表示法,其中 ...
表示無止盡的 9 個序列。 程式中的常數0.2
和0.3
也將是其真實值的近似值。剛好,最接近0.2
的double
大於有理數0.2
,但最接近double
code>0.3 小於有理數 0.3
。 0.1
和 0.2
的總和最終大於有理數 0.3
,因此與程式碼中的常數不一致。
浮點算術問題的相當全面的處理是每個計算機科學家都應該了解浮點運算。有關更容易理解的解釋,請參閱 floating-point-gui.de。
普通的舊十進制(以 10 為基數)數字也有同樣的問題,這就是為什麼像 1/3 這樣的數字最終會變成 0.333333333...
您剛剛偶然發現了一個數字 (3/10),它很容易用十進位表示,但不適合二進位系統。它也是雙向的(在某種程度上):1/16 在十進制中是一個醜陋的數字(0.0625),但在二進制中它看起來像十進制的第10,000 個(0.0001)** - 如果我們在由於我們在日常生活中使用以2 為基數的數字系統的習慣,您甚至會看到這個數字並本能地理解您可以通過將某個東西減半、再減半、一次又一次地達到這個數字。
當然,這並不完全是浮點數在記憶體中的儲存方式(它們使用科學計數法的形式)。然而,它確實說明了二進制浮點精度誤差往往會出現,因為我們通常感興趣的“現實世界”數字通常是十的冪 - 但這只是因為我們使用十進制數字系統日 -今天。這也是為什麼我們會說 71% 而不是「每 7 就有 5」(71% 是一個近似值,因為 5/7 無法用任何十進制數字精確表示)。
所以不行:二進制浮點數並沒有被破壞,它們只是碰巧和其他所有基於 N 的數字系統一樣不完美:)
實際上,這種精確度問題意味著您需要使用舍入函數將浮點數四捨五入到您感興趣的小數位數,然後再顯示它們。
您還需要用允許一定程度容差的比較來替換相等測試,這意味著:
不要不做if (x == y) { ... }
#而是執行 if (abs(x - y) < myToleranceValue) { ... }< myToleranceValue) { ... }
。
其中 abs
是絕對值。 myToleranceValue
需要根據您的特定應用程式進行選擇 - 這與您準備允許的「迴旋空間」有很大關係,以及您要比較的最大數字可能是多少是(由於精確度損失問題)。請注意您選擇的語言中的“epsilon”樣式常數。這些可以用作容差值,但它們的有效性取決於您正在使用的數字的大小(大小),因為大數字的計算可能會超出 epsilon 閾值。