簡體   English   中英

C#中的浮點數學是否一致? 是真的嗎?

[英]Is floating-point math consistent in C#? Can it be?

不,這不是另一個“為什么是(1/3.0)*3!= 1”問題。

最近,我一直在閱讀有關浮點的信息。 具體而言,相同的計算如何在不同的架構或優化設置上給出不同的結果

這是存儲重播或點對點網絡的視頻游戲的問題(與服務器 - 客戶相反),它依賴於每次運行程序時都會生成完全相同的結果的所有客戶 - 一個小的差異浮點計算可能會導致不同機器上的游戲狀態截然不同(甚至在同一台計算機上!

即使在“遵循” IEEE-754的處理器中,這種情況也會發生,主要是因為某些處理器(即X86)使用雙重擴展精度 也就是說,他們使用80位寄存器進行所有計算,然后將其截斷為64或32位,從而與使用64或32位進行計算的機器不同。

我在網上看到了這個問題的幾個解決方案,但都是針對 C++,而不是 C#:

  • 使用_controlfp_s (Windows), _FPU_SETCW (linux?)或fpsetprec (BSD)禁用雙重擴展模式(以便所有double計算使用IEEE-754 64位)使用IEEE-754 64位。
  • 始終使用相同的優化設置運行相同的編譯器,並要求所有用戶具有相同的CPU架構(無跨平台播放)。 因為我的“編譯器”實際上是JIT,每次運行程序時可能會以不同的方式優化,所以我認為這是不可能的。
  • 使用定點算術,避免float並完全double decimal將用於此目的,但要慢得多,並且沒有一個系統System.Math庫功能支持它。

那么,這甚至是C#中的問題嗎? 如果我只打算支持Windows(不是單聲道)怎么辦?

如果是這樣,有什么辦法強迫我的程序以正常的雙精度運行?

如果沒有,是否有任何可以幫助保持浮點計算一致的庫

我知道沒有辦法在 .net 中確定正常的浮點數。 JITter 允許創建在不同平台(或不同版本的 .net)上行為不同的代碼。 因此,在確定性 .net 代碼中使用普通float是不可能的。

我考慮的解決方法:

  1. 在 C# 中實現 FixedPoint32。 雖然這並不太難(我已經完成了一半的實現),但非常小的值范圍使其使用起來很煩人。 您必須始終小心,以免溢出,也不會丟失太多精度。 最后我發現這並不比直接使用整數更容易。
  2. 在 C# 中實現 FixedPoint64。 我發現這很難做到。 對於某些操作,128 位的中間整數會很有用。 但是 .net 不提供這種類型。
  3. 實現一個自定義的 32 位浮點。 在實現這一點時,缺少 BitScanReverse 內在函數會導致一些煩惱。 但目前我認為這是最有希望的路徑。
  4. 使用本機代碼進行數學運算。 對每個數學運算產生委托調用的開銷。

我剛剛開始了 32 位浮點數學的軟件實現。 在我的 2.66GHz i3 上,它每秒可以進行大約 7000 萬次加法/乘法運算。 https://github.com/CodesInChaos/SoftFloat 顯然它仍然非常不完整和錯誤。

C# 規范(§4.1.6 浮點類型)特別允許使用高於結果的精度進行浮點計算。 所以,不,我認為您不能直接在.Net 中使這些計算具有確定性。 其他人建議了各種解決方法,因此您可以嘗試一下。

在您需要此類操作的絕對可移植性的情況下,以下頁面可能很有用。 它討論了用於測試 IEEE 754 標准實現的軟件,包括用於模擬浮點運算的軟件。 然而,大多數信息可能特定於 C 或 C++。

http://www.math.utah.edu/~beebe/software/ieee/

關於固定點的說明

二進制定點數也可以很好地代替浮點數,這從四個基本算術運算中可以看出:

  • 加法和減法是微不足道的。 它們的工作方式與整數相同。 只需加或減!
  • 要將兩個定點數相乘,請將這兩個數相乘,然后右移定義的小數位數。
  • 要將兩個定點數相除,請將被除數左移定義的小數位數,然后除以除數。
  • 本文的第四章對實現二進制定點數有額外的指導。

二進制定點數可以在任何 integer 數據類型(例如 int、long 和 BigInteger)以及不符合 CLS 的類型 uint 和 ulong 上實現。

正如另一個答案中所建議的,您可以使用查找表,其中表中的每個元素都是二進制定點數,以幫助實現復雜的函數,例如正弦、余弦、平方根等。 如果查找表的粒度小於定點數,建議通過將查找表粒度的一半添加到輸入來對輸入進行四舍五入:

// Assume each number has a 12 bit fractional part. (1/4096)
// Each entry in the lookup table corresponds to a fixed point number
//  with an 8-bit fractional part (1/256)
input+=(1<<3); // Add 2^3 for rounding purposes
input>>=4; // Shift right by 4 (to get 8-bit fractional part)
// --- clamp or restrict input here --
// Look up value.
return lookupTable[input];

這是 C# 的問題嗎?

是的。 不同的架構是您最不必擔心的,不同的幀率等可能會由於浮點表示的不准確而導致偏差 - 即使它們是相同的不准確(例如,相同的架構,除了一台機器上較慢的 GPU )。

我可以使用 System.Decimal 嗎?

沒有理由你不能,但是它很慢。

有沒有辦法強制我的程序以雙精度運行?

是的。 自己托管 CLR 運行時 並在調用 CorBindToRuntimeEx 之前將所有必要的調用/標志(改變浮點運算的行為)編譯到 C++ 應用程序中。

是否有任何庫可以幫助保持浮點計算的一致性?

從來沒聽說過。

還有其他方法可以解決這個問題嗎?

我以前解決過這個問題,想法是使用QNumbers 它們是定點實數的一種形式; 但不是以 10 為底的定點(十進制) - 而是以 2 為底的(二進制); 因此,它們上的數學原語(add、sub、mul、div)比簡單的 base-10 固定點快得多; 特別是如果n對於兩個值都相同(在您的情況下是這樣)。 此外,由於它們是不可或缺的,它們在每個平台上都有明確定義的結果。

請記住,幀率仍然會影響這些,但它並沒有那么糟糕,並且可以使用同步點輕松糾正。

我可以在 QNumbers 中使用更多的數學函數嗎?

是的,往返一個小數來做到這一點。 此外,您真的應該為 trig (sin, cos) 函數使用查找表 因為它們確實可以在不同的平台上給出不同的結果 - 如果您正確編碼它們,它們可以直接使用 QNumbers。

根據這個稍微舊的MSDN 博客條目,JIT 不會將 SSE/SSE2 用於浮點,它都是 x87。 因此,正如您提到的,您必須擔心模式和標志,並且在 C# 中這是無法控制的。 因此,使用正常的浮點運算並不能保證您的程序在每台機器上都得到完全相同的結果。

要獲得雙精度的精確再現性,您將不得不進行軟件浮點(或定點)仿真。 我不知道 C# 庫可以做到這一點。

根據您需要的操作,您可能能夠以單精度逃脫。 這是想法:

  • 以單精度存儲您關心的所有值
  • 執行操作:
    • 將輸入擴展到雙精度
    • 雙精度運算
    • 將結果轉換回單精度

x87 的最大問題是計算可能以 53 位或 64 位精度完成,具體取決於精度標志以及寄存器是否溢出到 memory。 但是對於許多運算,以高精度執行運算並舍入到較低精度將保證正確的答案,這意味着答案將保證在所有系統上都是相同的。 是否獲得額外的精度無關緊要,因為無論哪種情況,您都有足夠的精度來保證正確的答案。

應該在這個方案中工作的操作:加法、減法、乘法、除法、sqrt。 像 sin、exp 之類的東西不會起作用(結果通常會匹配,但不能保證)。 “什么時候雙舍入是無害的?” ACM 參考(付費注冊請求)

希望這可以幫助!

正如其他答案已經說明的那樣:是的,這是 C# 中的一個問題 - 即使保持純 Windows 也是如此。

至於解決方案:如果您使用內置的BigInteger class 並通過對此類數字的任何計算/存儲使用公分母將所有計算縮放到定義的精度,則可以減少(並通過一些努力/性能影響)完全避免該問題.

根據 OP 的要求 - 關於性能:

System.Decimal表示帶有 1 位符號和 96 位 Integer 和“刻度”(表示小數點所在的位置)的數字。 對於您進行的所有計算,它必須在此數據結構上運行,並且不能使用 CPU 中內置的任何浮點指令。

BigInteger “解決方案”做了類似的事情——只是你可以定義你需要/想要多少位數......也許你只需要 80 位或 240 位的精度。

緩慢總是來自於必須通過僅整數指令模擬對這些數字的所有操作,而不使用 CPU/FPU 內置指令,這反過來又導致每個數學運算的指令更多。

為了減少對性能的影響,有幾種策略——比如 QNumbers(參見 Jonathan Dickinson 的回答—— 浮點數學在 C# 中是否一致?可以嗎? )和/或緩存(例如三角計算......)等。

好吧,這將是我第一次嘗試如何做到這一點

  1. 創建一個 ATL.dll 項目,其中包含一個簡單的 object 用於您的關鍵浮點運算。 確保使用禁止使用任何非 xx87 硬件進行浮點運算的標志對其進行編譯。
  2. 創建調用浮點運算並返回結果的函數; 從簡單開始,然后如果它對您有用,您可以隨時增加復雜性以滿足您以后的性能需求。
  3. 將 control_fp 調用放在實際數學周圍,以確保它在所有機器上以相同的方式完成。
  4. 參考您的新庫並進行測試以確保它按預期工作。

(我相信您可以編譯為 32 位。dll,然后將其與 x86 或 AnyCpu 一起使用 [或者可能僅針對 x86 系統;請參閱下面的評論 64。]

然后,假設它有效,您是否想使用 Mono 我想您應該能夠以類似的方式在其他 x86 平台上復制該庫(不是 COM,當然,我的區域有一點點?我們 go 那里雖然......)。

假設您可以使其工作,您應該能夠設置可以一次執行多個操作以解決任何性能問題的自定義函數,並且您將擁有浮點數學,允許您以最少的數量在平台上獲得一致的結果用 C++ 編寫的代碼,並將代碼的 rest 留在 C# 中。

我不是游戲開發者,雖然我確實有很多計算難題的經驗……所以,我會盡力而為。

我將采用的策略基本上是這樣的:

  • 使用較慢(如有必要;如果有更快的方法,很好),但可預測的方法來獲得可重復的結果
  • 對其他一切使用雙精度(例如,渲染)

這件事的短處是:你需要找到一個平衡點。 如果您花費 30 毫秒渲染 (~33fps) 並且僅 1 毫秒進行碰撞檢測(或插入一些其他高度敏感的操作)——即使您將執行關鍵算術所需的時間增加三倍,它對您的幀速率的影響是你從 33.3fps 下降到 30.3fps。

我建議您對所有內容進行概要分析,說明每個明顯昂貴的計算花費了多少時間,然后使用一種或多種解決此問題的方法重復測量,看看會產生什么影響。

檢查其他答案中的鏈接可以清楚地表明,您永遠無法保證浮點是否“正確”實現,或者您是否總是會為給定的計算獲得一定的精度,但也許您可以盡最大努力(1) 將所有計算截斷到一個共同的最小值(例如,如果不同的實現將為您提供 32 到 80 位的精度,則總是將每個操作截斷為 30 或 31 位),(2)在啟動時有一個包含幾個測試用例的表(加、減、乘、除、sqrt、余弦等的邊界情況)並且如果實現計算與表匹配的值,則不必費心進行任何調整。

你的問題是相當困難和技術性的東西 O_o。 不過我可能有一個想法。

您肯定知道 CPU 在任何浮動操作后都會進行一些調整。 CPU提供了幾種不同的指令來進行不同的舍入操作。

所以對於一個表達式,你的編譯器會選擇一組指令來引導你得到一個結果。 但是任何其他指令工作流程,即使他們打算計算相同的表達式,也可以提供另一個結果。

由四舍五入調整所造成的“錯誤”會隨着每一次進一步的指示而增加。

例如,我們可以說在裝配級別:a * b * c 不等於 a * c * b。

我對此並不完全確定,您需要詢問比我更了解 CPU 架構的人:p

但是要回答您的問題:在 C 或 C++ 中,您可以解決您的問題,因為您可以控制編譯器生成的機器代碼,但是在 .NET 中您沒有任何控制。 因此,只要您的機器代碼可能不同,您就永遠無法確定確切的結果。

我很好奇這會以何種方式成為問題,因為變化似乎非常小,但如果您需要真正准確的操作,我能想到的唯一解決方案是增加浮動寄存器的大小。 如果可以的話,使用雙精度甚至長雙精度(不確定是否可以使用 CLI)。

我希望我已經足夠清楚了,我的英語並不完美(......根本:s)

暫無
暫無

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

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