Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The Microsoft ACE driver changes the floating point precision in the rest of my program

I am having a problem where it seems that the results of some calculations change after having used the Microsoft ACE driver to open an Excel spreadsheet.

The code below reproduces the problem.

The first two calls to DoCalculation yield the same results. Then I call the function OpenSpreadSheet which opens and closes an Excel 2003 spreadsheet using the ACE driver. You would not expect OpenSpreadSheet to have any effect on the last call to DoCalculation but it turns out that the result actually changes. This is the output that the program generates:

1,59142713593566
1,59142713593566
1,59142713593495

Note the differences on the last 3 decimals. This does not seem like a big difference but in our production code the calculations are complex and the resulting differences are quite large.

It makes no difference if I use the JET driver instead of the ACE driver. If I change the types from double to decimal the error goes away. But this is not an option in our production code.

I am running on a Windows 7 64 bit and the assemblies are compiled for .NET 4.5 x86. Using the 64 bit ACE driver is not an option as we are running 32 bit Office.

Does anybody know why this is happening and how I can fix it?

The following code reproduces my problem:

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
        }
    }
}
like image 476
Jakob Christensen Avatar asked Jun 04 '13 10:06

Jakob Christensen


1 Answers

This is technically possible, unmanaged code may be tinkering with the FPU control word and change the way it calculates. Well-known trouble makers are DLLs compiled with Borland tools, their runtime support code unmasks exceptions that can crash managed code. And DirectX, it is known for tinkering with the FPU control word to get calculations with double to be performed as float to speed up graphics math.

The specific kind of FPU control word change that appears to be made here is the rounding mode, used by the FPU when it needs to write an internal register value with 80-bit precision to a 64-bit memory location. It has 4 options to make that conversion: round up, round down, truncate and round-to-even (banker's rounding). Very small differences but you do make an effort to accumulate them rapidly. And if your numerical model is unstable then you certainly will see a difference in the end result. That doesn't make it more or less accurate, just different.

Managed code is pretty defenseless against code that does this, you cannot directly access the FPU control word. It requires writing assembly code. You've got one trick available, highly undocumented but pretty effective. The CLR will reset the FPU whenever it handles an exception. So you could do this:

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) {}
}

Do beware that this is expensive so use as infrequently as possible. And it is a major pita when you debug code so you might want to disable this in the Debug build.

I should mention an alternative, you can pinvoke the _fpreset() function in msvcrt.dll. It is however risky if you use it inside of a method that also performs floating point math, the jitter optimizer doesn't know that this function jerks the floor mat. You'll need to thoroughly test the Release build:

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

And do keep in mind that this does not make your calculation results more accurate in any way. Just different. Just like running the Release build of your code without a debugger will produce different results than the Debug build. The Release build code will perform this kind of rounding less frequently since the jitter optimizer makes an effort to keep intermediate results inside the FPU at 80-bit precision. Producing a different result from the Debug build but one that actually is more accurate. Give or take. This 80-bit intermediate format was Intel's billion dollar mistake, not repeated in the SSE2 instruction set.

like image 154
Hans Passant Avatar answered Sep 20 '22 20:09

Hans Passant