簡體   English   中英

浮點誤差的確定性如何?

[英]How deterministic is floating point inaccuracy?

我知道浮點計算存在准確性問題,並且有很多問題可以解釋原因。 我的問題是,如果我兩次運行相同的計算,我是否可以始終依賴它來產生相同的結果? 哪些因素可能會影響這一點?

  • 計算之間的時間?
  • CPU的當前狀態?
  • 不同的硬件?
  • 語言/平台/操作系統?
  • 太陽耀斑?

我有一個簡單的物理模擬,想記錄會話以便可以重播。 如果可以依賴計算,那么我應該只需要記錄初始狀態和任何用戶輸入,並且我應該始終能夠准確地重現最終狀態。 如果計算不准確,則開始時的錯誤可能會在模擬結束時產生巨大影響。

我目前在 Silverlight 工作,但很想知道這個問題是否可以得到一般性回答。

更新:最初的答案表明是,但顯然這並不完全明確,正如所選答案的評論中所討論的那樣。 看起來我將不得不做一些測試,看看會發生什么。

據我所知,只要您處理相同的指令集和編譯器,並且您運行的任何處理器都嚴格遵守相關標准(即 IEEE754),那么您只能保證獲得相同的結果。 也就是說,除非您正在處理一個特別混亂的系統,否則運行之間的任何計算漂移都不太可能導致錯誤行為。

我知道的具體問題:

  1. 某些操作系統允許您以破壞兼容性的方式設置浮點處理器的模式。

  2. 浮點中間結果通常在寄存器中使用 80 位精度,但在內存中僅使用 64 位。 如果程序以更改函數內寄存器溢出的方式重新編譯,則與其他版本相比,它可能返回不同的結果。 大多數平台都會為您提供一種強制將所有結果截斷為內存精度的方法。

  3. 標准庫函數可能會因版本而異。 我認為在 gcc 3 vs 4 中有一些並不少見的例子。

  4. IEEE 本身允許一些二進制表示不同......特別是 NaN 值,但我不記得細節。

簡短的回答是,根據IEEE 浮點標准,FP 計算完全是確定性的,但這並不意味着它們可以在機器、編譯器、操作系統等之間完全重現。

這些問題的詳細答案以及更多問題可以在可能是關於浮點數的最佳參考資料中找到,David Goldberg 的《每個計算機科學家應該知道的關於浮點運算的知識》 跳到有關 IEEE 標准的部分了解關鍵細節。

簡要回答您的要點:

  • 計算和 CPU 狀態之間的時間與此無關。

  • 硬件會影響事物(例如,某些 GPU 不符合 IEEE 浮點標准)。

  • 語言、平台和操作系統也會影響事物。 要獲得比我能提供的更好的描述,請參閱 Jason Watkins 的回答。 如果您使用的是 Java,請查看 Kahan對 Java 的浮點不足之處咆哮

  • 太陽耀斑可能很重要,希望很少發生。 我不會太擔心,因為如果它們真的很重要,那么其他一切也都被搞砸了。 我會將其與擔心EMP放在同一類別中。

最后,如果您在相同的初始輸入上執行相同的浮點計算序列,那么事情應該完全可以重播。 確切的順序可能會因您的編譯器/操作系統/標准庫而異,因此您可能會以這種方式遇到一些小錯誤。

如果您有一個數值不穩定的方法,並且您從近似相同但不完全相同的 FP 輸入開始,那么您通常會遇到浮點問題。 如果您的方法穩定,您應該能夠保證在一定容差范圍內的重現性。 如果您想要比這更詳細的信息,請查看上面鏈接的 Goldberg 的 FP 文章或獲取有關數值分析的介紹性文本。

我認為您的困惑在於浮點數的不准確類型。 大多數語言都實現了IEEE 浮點標准。該標准規定了如何使用 float/double 中的各個位來生成數字。 通常一個浮點數由一個四字節和一個雙八字節組成。

兩個浮點數之間的數學運算每次都將具有相同的值(如標准中所指定)。

不准確在於精度。 考慮一個 int 和一個浮點數。 兩者通常占用相同數量的字節 (4)。 然而,每個數字可以存儲的最大值卻大不相同。

  • 整數:大約 20 億
  • 浮點數:3.40282347E38(大一點)

區別在中間。 int,可以表示 0 到大約 20 億之間的每個數字。 但是浮動不能。 它可以表示 0 到 3.40282347E38 之間的 20 億個值。 但這留下了無法表示的整個范圍的值。 如果數學方程達到這些值之一,則必須將其四舍五入為可表示的值,因此被認為是“不准確的”。 您對不准確的定義可能會有所不同:)。

此外,雖然Goldberg是一個很好的參考,但原文也是錯誤的: IEEE754 is not gaurenteed to beportable 鑒於此聲明是基於瀏覽文本的頻率,我再怎么強調也不為過。 該文檔的更高版本包括專門討論此問題的部分

許多程序員可能沒有意識到,即使是僅使用 IEEE 標准規定的數字格式和運算的程序,也可以在不同的系統上計算出不同的結果。 事實上,該標准的作者旨在允許不同的實現獲得不同的結果。

C++ FAQ 中的這個答案可能是最好的描述:

http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18

不僅不同的體系結構或編譯器可能會給您帶來麻煩,浮點數在同一程序中已經以奇怪的方式運行。 正如 FAQ 指出的,如果y == x為真,那仍然可能意味着cos(y) == cos(x)為假。 這是因為 x86 CPU 使用 80 位計算值,而該值在內存中存儲為 64 位,因此您最終將截斷的 64 位值與完整的 80 位值進行比較。

計算仍然是確定性的,從某種意義上說,運行相同的編譯二進制文件每次都會給你相同的結果,但是當你稍微調整源代碼、優化標志或使用不同的編譯器編譯它時,所有賭注都關閉了,任何事情可以發生。

實際上,我並沒有那么糟糕,我可以在 32 位 Linux 上一點一點地使用不同版本的 GCC 重現簡單的浮點數學,但是當我切換到 64 位 Linux 時,結果不再相同。 在 32 位上創建的演示錄音不能在 64 位上工作,反之亦然,但在同一架構上運行時可以正常工作。

對不起,但我不禁認為每個人都沒有抓住重點。

如果不准確對您正在做的事情很重要,那么您應該尋找不同的算法。

你說如果計算不准確,開始時的錯誤可能會在模擬結束時產生巨大的影響。

那個我的朋友不是模擬的。 如果由於四舍五入和精度的微小差異而得到截然不同的結果,那么很可能沒有任何結果具有任何有效性。 僅僅因為您可以重復結果並不能使其更有效。

對於包括測量或非整數計算在內的任何重要的現實世界問題,引入小錯誤以測試算法的穩定性始終是一個好主意。

由於您的問題被標記為 C#,因此值得強調 .NET 面臨的問題:

  1. 浮點數學不是結合的——也就是說, (a + b) + c不能保證等於a + (b + c)
  2. 不同的編譯器將以不同的方式優化您的代碼,這可能涉及重新排序算術運算。
  3. 在 .NET 中,CLR 的 JIT 編譯器將即時編譯您的代碼,因此編譯取決於運行時機器上的 .NET 版本。

這意味着,當在不同版本的 .NET CLR 上運行時,您不應依賴 .NET 應用程序產生相同的浮點計算結果。

例如,在您的情況下,如果您記錄模擬的初始狀態和輸入,然后安裝更新 CLR 的服務包,則您的模擬在下次運行時可能不會以相同的方式重放。

請參閱 Shawn Hargreaves 的博客文章浮點數學確定性嗎? .NET 相關的進一步討論。

嗯。 由於 OP 要求使用 C#:

C# 字節碼 JIT 是確定性的還是在不同的運行之間生成不同的代碼? 我不知道,但我不會相信 Jit。

我可以想到 JIT 具有某些服務質量特性並決定在優化上花費更少時間的場景,因為 CPU 在其他地方進行大量的數字運算(想想背景 DVD 編碼)? 這可能會導致細微的差異,而這些差異可能會在以后導致巨大的差異。

此外,如果 JIT 本身得到改進(也許作為服務包的一部分),生成的代碼肯定會改變。 已經提到了 80 位內部精度問題。

這不是您問題的完整答案,但這里有一個示例,說明 C# 中的雙重計算是不確定的。 我不知道為什么,但看似無關的代碼顯然會影響下游雙重計算的結果。

  1. 在 Visual Studio 版本 12.0.40629.00 Update 5 中創建一個新的 WPF 應用程序,並接受所有默認選項。
  2. 將 MainWindow.xaml.cs 的內容替換為:

     using System; using System.Windows; namespace WpfApplication1 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Content = FooConverter.Convert(new Point(950, 500), new Point(850, 500)); } } public static class FooConverter { public static string Convert(Point curIPJos, Point oppIJPos) { var ij = " Insulated Joint"; var deltaX = oppIJPos.X - curIPJos.X; var deltaY = oppIJPos.Y - curIPJos.Y; var teta = Math.Atan2(deltaY, deltaX); string result; if (-Math.PI / 4 <= teta && teta <= Math.PI / 4) result = "Left" + ij; else if (Math.PI / 4 < teta && teta <= Math.PI * 3 / 4) result = "Top" + ij; else if (Math.PI * 3 / 4 < teta && teta <= Math.PI || -Math.PI <= teta && teta <= -Math.PI * 3 / 4) result = "Right" + ij; else result = "Bottom" + ij; return result; } } }
  3. 將生成配置設置為“發布”並生成,但不在 Visual Studio 中運行。

  4. 雙擊生成的 exe 運行它。
  5. 請注意,窗口顯示“底部絕緣接頭”。
  6. 現在在“字符串結果”之前添加這一行:

     string debug = teta.ToString();
  7. 重復步驟 3 和 4。

  8. 請注意,窗口顯示“右絕緣接頭”。

這種行為在同事的機器上得到了證實。 請注意,如果以下任一情況為真,則窗口始終顯示“Right Insulated Joint”:exe 從 Visual Studio 中運行,exe 是使用 Debug 配置構建的,或者在項目屬性中取消選中“Prefer 32-bit”。

很難弄清楚發生了什么,因為任何觀察過程的嘗試似乎都會改變結果。

很少有 FPU 符合 IEEE 標准(盡管他們聲稱)。 所以在不同的硬件上運行相同的程序確實會給你不同的結果。 結果很可能出現在極端情況下,作為在軟件中使用 FPU 的一部分,您應該已經避免這些情況。

IEEE 錯誤通常在軟件中修補,您確定您今天運行的操作系統包含制造商提供的正確陷阱和補丁嗎? 操作系統更新之前或之后呢? 是否刪除了所有錯誤並添加了錯誤修復? C 編譯器是否與所有這些同步?C 編譯器是否生成正確的代碼?

對此進行測試可能是徒勞的。 在您交付產品之前,您不會看到問題。

遵守 FP 規則 1:永遠不要使用 if(something==something) 比較。 IMO 的第二條規則與 ascii 到 fp 或 fp 到 ascii(printf、scanf 等)有關。 那里有比硬件更多的准確性和錯誤問題。

隨着每一代硬件(密度)的出現,來自太陽的影響更加明顯。 我們已經在行星表面上遇到了 SEU 的問題,因此獨立於浮點計算,您會遇到問題(很少有供應商願意關心,因此預計新硬件會更頻繁地崩潰)。

通過消耗大量邏輯,fpu 可能會非常快(單個時鍾周期)。 不比整數 alu 慢。 不要將此與現代 fpus 像 alus 一樣簡單混淆,fpus 很昂貴。 (alus 同樣消耗更多的乘法和除法邏輯以將其降低到一個時鍾周期,但它幾乎沒有 fpu 大)。

遵循上面的簡單規則,多研究一點浮點數,了解隨之而來的問題和陷阱。 您可能需要定期檢查無窮大或 nans。 與硬件相比,您的問題更可能在編譯器和操作系統中發現(通常不僅僅是 fp 數學)。 根據定義,現代硬件(和軟件)現在充滿了錯誤,所以只要嘗試比您的軟件運行時更少錯誤即可。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM