【问题标题】:The Microsoft ACE driver changes the floating point precision in the rest of my programMicrosoft ACE 驱动程序更改了我程序其余部分的浮点精度
【发布时间】:2013-05-30 17:53:00
【问题描述】:

我遇到了一个问题,在使用 Microsoft ACE driver 打开 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
        }
    }
}

【问题讨论】:

    标签: .net excel floating-point jet


    【解决方案1】:

    这在技术上是可行的,非托管代码可能会修改 FPU 控制字并更改其计算方式。众所周知的麻烦制造者是使用 Borland 工具编译的 DLL,它们的运行时支持代码会取消屏蔽可能导致托管代码崩溃的异常。而 DirectX,它以修改 FPU 控制字以将 double 的计算作为 float 执行以加快图形数学运算而闻名。

    这里出现的 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 指令集中没有重复。

    【讨论】:

    • 感谢您的详尽回答。我意识到一个结果并不比另一个更正确,但问题是 ACE 驱动程序具有这些副作用,导致在同一过程中完成的计算发生变化。我试过你的方法,它们都有效。异常方法仅适用于调试版本。感谢您的宝贵时间。
    • 嗯,不,此重置也在发布版本中完成。它位于 CLR 内部,代码不受您构建程序的方式的影响。请记住最后一段,您将在发布版本中获得不同的结果,具体取决于您是否附加了调试器。
    • 哦,我的错。我的意思是说 exeption 方法仅适用于 RELEASE 构建 - 而不是调试构建。
    • 喜欢总是最好的之一!谢谢
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多