Microsoft ACE驱动程序更改程序其余部分的浮点精度

时间:2013-06-04 10:58:19

标签: .net excel floating-point jet

我遇到一个问题,在使用Microsoft ACE driver打开Excel电子表格后,某些计算结果似乎发生了变化。

下面的代码重现了这个问题。

DoCalculation的前两次调用会产生相同的结果。然后我调用函数OpenSpreadSheet,它使用ACE驱动程序打开和关闭Excel 2003电子表格。您不希望OpenSpreadSheet对最后一次调用DoCalculation产生任何影响,但事实证明结果实际上发生了变化。这是程序生成的输出:

1,59142713593566
1,59142713593566
1,59142713593495

请注意最后3位小数的差异。这似乎不是一个很大的区别,但在我们的生产代码中,计算很复杂,产生的差异非常大。

如果我使用JET驱动程序而不是ACE驱动程序没有区别。如果我将类型从double更改为十进制,则错误消失。但这不是我们的生产代码中的一个选项。

我在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
        }
    }
}

1 个答案:

答案 0 :(得分:16)

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

这里似乎要进行的特定类型的FPU控制字更改是舍入模式,当需要将具有80位精度的内部寄存器值写入64位存储器位置时,FPU使用该舍入模式。它有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) {}
}

请注意这是昂贵的,因此请尽可能少用。调试代码时它是一个主要的皮塔,因此您可能希望在Debug构建中禁用它。

我应该提一个替代方案,你可以在msvcrt.dll中调用_fpreset()函数。但是,如果在同样执行浮点数学运算的方法中使用它,则风险很大,抖动优化器不知道此函数会使地垫抖动。您需要彻底测试Release版本:

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

请记住,会使您的计算结果更加准确。只是不同。就像在没有调试器的情况下运行代码的Release版本将产生与Debug构建不同的结果。由于抖动优化器努力将中间结果保持在FPU内以80位精度,因此Release构建代码将不太频繁地执行这种舍入。从Debug构建产生不同的结果,但实际上更准确。给予或接受。这种80位中间格式是英特尔十亿美元的错误,在SSE2指令集中没有重复。