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指令集中没有重复.
| 归档时间: |
|
| 查看次数: |
946 次 |
| 最近记录: |