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

.net Microsoft ACE驱动程序在程序的其余部分更改浮点精度,.net,excel,floating-point,jet,.net,Excel,Floating Point,Jet,我遇到了一个问题,在使用打开Excel电子表格后,某些计算的结果似乎发生了变化 下面的代码再现了这个问题 对doccalculation的前两个调用产生相同的结果。然后调用函数OpenSpreadSheet,该函数使用ACE驱动程序打开和关闭Excel 2003电子表格。您不会期望OpenSpreadSheet对上次调用doccalculation有任何影响,但结果确实发生了变化。这是程序生成的输出: 1,59142713593566 1,59142713593566 1,59142713593

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

下面的代码再现了这个问题

doccalculation
的前两个调用产生相同的结果。然后调用函数
OpenSpreadSheet
,该函数使用ACE驱动程序打开和关闭Excel 2003电子表格。您不会期望
OpenSpreadSheet
对上次调用
doccalculation
有任何影响,但结果确实发生了变化。这是程序生成的输出:

1,59142713593566
1,59142713593566
1,59142713593495
注意最后3位小数的差异。这看起来并没有太大的区别,但在我们的生产代码中,计算是复杂的,并且产生的差异相当大

如果我使用喷气式飞机驾驶员而不是王牌驾驶员,这没有什么区别。如果我将类型从double改为decimal,错误就会消失。但这在我们的生产代码中不是一个选项

我运行的是64位Windows 7,程序集是为.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
        }
    }
}
static void Main(字符串[]args)
{
doccalculation();
doccalculation();
OpenSpreadSheet();
doccalculation();
}
静态空隙率计算()
{
//将两个随机选择的数字相乘10.000倍。
变量d1=1.0003123132;
变量d3=0.999734234;
双res=1;
对于(int i=0;i<10000;i++)
{
res*=d1*d3;
}
控制台写入线(res);
}
公共静态void OpenSpreadSheet()
{
var cn=new-OleDbConnection(@“Provider=Microsoft.ACE.OLEDB.12.0;数据源=c:\temp\workbook1.xls;扩展属性=excel8.0”);
var cmd=new OleDbCommand(“从[Sheet1$]中选择[Column1]”,cn);
cn.Open();
使用(cn)
{
使用(OleDbDataReader=cmd.ExecuteReader())
{
//无所事事
}
}
}

这在技术上是可能的,非托管代码可能会修补FPU控制字并更改其计算方式。众所周知的麻烦制造者是使用Borland工具编译的DLL,它们的运行时支持代码会揭露可能导致托管代码崩溃的异常。和DirectX,它以修补FPU控制字而闻名,以使计算以双精度浮点运算来执行,从而加速图形数学

此处出现的特定类型的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) {}
}
一定要小心,这是昂贵的,所以使用尽可能少。当您调试代码时,它是一个主要的pita,因此您可能希望在调试构建中禁用它

我应该提到另一种选择,您可以在msvcrt.dll中pinvoke _fpreset()函数。但是,如果在同时执行浮点运算的方法中使用该函数,则会有风险,抖动优化器不知道该函数会使地板震动。您需要彻底测试发布版本:

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

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

感谢您的详尽回答。我意识到一个结果并不比另一个更正确,但问题是ACE驱动程序有这些副作用,导致在同一过程中进行的计算发生变化。我试过你的方法,它们都有效。但是,exception方法仅适用于调试生成。谢谢你的时间。嗯,不,这个重置也在发布版本中完成。它位于CLR内部,即不受构建程序方式影响的代码。请记住最后一段,在发布版本中会得到不同的结果,这取决于是否附加了调试器。哦,我的错误。我想说的是,execption方法只适用于发布版本,而不是调试版本。它总是最好的方法之一!谢谢