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

Jak*_*sen 5 .net floating-point excel jet

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

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

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

1,59142713593566
1,59142713593566
1,59142713593495
Run Code Online (Sandbox Code Playgroud)

请注意最后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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Han*_*ant 16

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

这里看来特定类型的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) {}
}
Run Code Online (Sandbox Code Playgroud)

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

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

    [System.Runtime.InteropServices.DllImport("msvcrt.dll")]
    public static extern void _fpreset();
Run Code Online (Sandbox Code Playgroud)

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

  • 喜欢永远是最好的之一!谢谢 (2认同)