微软ACE驱动程序改变了我的程序中其余部分的浮点精度。

7
我遇到了一个问题,似乎在使用Microsoft ACE驱动程序打开Excel电子表格后,一些计算结果发生了变化。
下面的代码重现了这个问题。
前两次调用DoCalculation产生相同的结果。然后我调用函数OpenSpreadSheet,使用ACE驱动程序打开并关闭Excel 2003电子表格。你不会期望OpenSpreadSheet对最后一次调用DoCalculation产生任何影响,但结果实际上改变了。这是程序生成的输出:
1,59142713593566
1,59142713593566
1,59142713593495

请注意最后三位小数的差异。这似乎不是很大的差异,但在我们的生产代码中,计算非常复杂,导致的差异相当大。
如果我使用JET驱动程序而不是ACE驱动程序,没有任何区别。如果我将类型从双精度更改为十进制,则错误消失了。但这在我们的生产代码中不是一个选项。
我正在运行Windows 7 64位,程序集编译为.NET 4.5 x86。由于我们正在运行32位Office,因此不能使用64位ACE驱动程序。
有人知道为什么会发生这种情况以及如何解决吗?
以下代码重现了我的问题:
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个回答

17

从技术上讲,未经管理的代码可能会调整FPU控制字并更改其计算方式。众所周知,使用Borland工具编译的DLL是麻烦制造者,它们的运行时支持代码会揭示可以使托管代码崩溃的异常。 而DirectX则以调整FPU控制字来以float的方式执行double计算以加速图形计算而闻名。

这里似乎所做的特定类型的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) {}
}

请注意,这个操作很昂贵,因此尽可能少用。在调试代码时可能会带来很大的麻烦,所以您可能希望在调试版本中禁用它。

我应该提到另一种替代方法,您可以调用msvcrt.dll中的_fpreset()函数。不过如果您在执行浮点数计算的方法中使用它,那么这是有风险的,JIT优化器无法知道这个函数会扰乱代码的运行顺序。您需要彻底测试发布版本:

    [System.Runtime.InteropServices.DllImport("msvcrt.dll")]
    public static extern void _fpreset();
请注意,这并不会以任何方式使您的计算结果更准确。只是不同而已。就像在没有调试器的情况下运行您代码的发布版本将产生不同的结果一样。 发布版本的代码会执行这种舍入操作较少,因为即时编译器优化器会努力保持FPU内部的中间结果精度为80位。 这会产生与调试版本不同但实际上更准确的结果。 大体上如此。这种80位的中间格式是英特尔的十亿美元错误,在SSE2指令集中没有重复。

感谢您的详细回答。我意识到一个结果并不比另一个更正确,但是ACE驱动程序具有这些副作用,会导致在同一进程中进行的计算发生变化,这是一个问题。我尝试了您的方法,它们都有效。但是异常处理方法只适用于调试版本。感谢您的时间。 - Jakob Christensen
嗯,不,这个重置也在发布版本中完成。它位于CLR内部,这段代码不受你构建程序的方式影响。请记住最后一段话,你在发布版本中会得到不同的结果,这取决于你是否连接了调试器。 - Hans Passant
哦,我的错误。我想说的是异常方法只适用于发布版本,而不适用于调试版本。 - Jakob Christensen
2
像往常一样,最棒的之一!谢谢。 - Luis Filipe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接