7

我遇到了一个问题,在使用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
        }
    }
}
4

1 回答 1

17

这在技术上是可行的,非托管代码可能会修改 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 指令集中没有重复。

于 2013-06-04T12:13:09.360 回答