Dot-Net

Microsoft ACE 驅動程序更改了我程序其餘部分的浮點精度

  • September 25, 2020

我遇到了一個問題,在使用Microsoft ACE 驅動程序打開 Excel 電子表格後,某些計算的結果似乎發生了變化。

下面的程式碼重現了該問題。

前兩個呼叫DoCalculation產生相同的結果。然後我呼叫OpenSpreadSheet使用 ACE 驅動程序打開和關閉 Excel 2003 電子表格的函式。您不會期望OpenSpreadSheet對最後一次呼叫有任何影響,DoCalculation但事實證明結果實際上發生了變化。這是程序生成的輸出:

1,59142713593566
1,59142713593566
1,59142713593495

注意最後 3 位小數的差異。這看起來差別不大,但在我們的生產程式碼中,計算很複雜,由此產生的差異非常大。

如果我使用 JET 驅動程序而不是 ACE 驅動程序,這沒有什麼區別。如果我將類型從 double 更改為 decimal,錯誤就會消失。但這不是我們的生產程式碼中的選項。

我在 Windows 7 64 位上執行,程序集是為 .NET 4.5 x86 編譯的。使用 64 位 ACE 驅動程序不是一個選項,因為我們正在執行 32 位 Office。

有誰知道為什麼會發生這種情況以及我該如何解決?

以下程式碼重現了我的問題:

static void Main(string[] args)
{
   DoCalculation();
   DoCalculation();
   OpenSpreadSheet();
   DoCalculation();
}

static void DoCalculation()
{
   // Multiply two randomly chosen number 10.000 times.
   var d1 = 1.0003123132;
   var d3 = 0.999734234;

   double res = 1;
   for (int i = 0; i < 10000; i++)
   {
       res *= d1 * d3;
   }
   Console.WriteLine(res);
}

public static void OpenSpreadSheet()
{
   var cn = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0;data source=c:\temp\workbook1.xls;Extended Properties=Excel 8.0");
   var cmd = new OleDbCommand("SELECT [Column1] FROM [Sheet1$]", cn);
   cn.Open();

   using (cn)
   {
       using (OleDbDataReader reader = cmd.ExecuteReader())
       {
           // Do nothing
       }
   }
}

這在技術上是可行的,非託管程式碼可能會修改 FPU 控製字並更改其計算方式。眾所周知的麻煩製造者是使用 Borland 工具編譯的 DLL,它們的執行時支持程式碼會取消屏蔽可能導致託管程式碼崩潰的異常。和 DirectX,它以修改 FPU 控製字以將精度計算作為浮點數執行以加速圖形數學而聞名。

此處出現的 FPU 控製字更改的具體類型是捨入模式,當 FPU 需要將具有 80 位精度的內部寄存器值寫入 64 位記憶體位置時,它使用該模式。它有 4 個選項來進行這種轉換:向上舍入、向下舍入、截斷和舍入到偶數(銀行家的捨入)。非常小的差異,但你確實努力快速積累它們。如果你的數值模型不穩定,那麼你肯定會看到最終結果的不同。這並不能使它或多或少準確,只是不同。

託管程式碼對執行此操作的程式碼毫無防備,您無法直接訪問 FPU 控製字。它需要編寫彙編程式碼。你有一個可用的技巧,高度無證但非常有效。CLR 將在處理異常時*重置FPU。*所以你可以這樣做:

public static void ResetMathProcessor() 
{
   if (IntPtr.Size != 4) return;   // No need in 64-bit code, it uses SSE
   try {
       throw new Exception("Please ignore, resetting the FPU");
   }
   catch (Exception ex) {}
}

請注意,這很昂貴,因此請盡可能少地使用。當您調試程式碼時,它是一個主要的 pita,因此您可能希望在 Debug 建構中禁用它。

我應該提到一個替代方案,您可以在 msvcrt.dll 中呼叫 _fpreset() 函式。但是,如果您在也執行浮點數學的方法中使用它是有風險的,抖動優化器不知道這個函式會抖動地板墊。您需要徹底測試發布版本:

   [System.Runtime.InteropServices.DllImport("msvcrt.dll")]
   public static extern void _fpreset();

請記住,這不會任何方式使您的計算結果更加準確。只是不同。就像在沒有調試器的情況下執行程式碼的 Release 版本會產生與 Debug 版本不同的結果。Release 建構程式碼將不那麼頻繁地執行這種舍入,因為抖動優化器會努力將 FPU 中的中間結果保持在 80 位精度。產生與 Debug 建構不同的結果,但實際上更準確。給予或接受。這種 80 位中間格式是英特爾的十億美元錯誤,在 SSE2 指令集中沒有重複。

引用自:https://stackoverflow.com/questions/16916073