我遇到一个问题,在使用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
}
}
}
答案 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指令集中没有重复。