2013-06-04 8 views
5

Ho un problema in cui sembra che i risultati di alcuni calcoli cambino dopo aver utilizzato il Microsoft ACE driver per aprire un foglio di calcolo di Excel.Il driver Microsoft ACE modifica la precisione in virgola mobile nel resto del mio programma

Il codice restituisce il problema.

Le prime due chiamate a DoCalculation producono gli stessi risultati. Quindi chiamo la funzione OpenSpreadSheet che apre e chiude un foglio di calcolo Excel 2003 utilizzando il driver ACE. Non ti aspetteresti che OpenSpreadSheet abbia alcun effetto sull'ultima chiamata a DoCalculation ma risulta che il risultato effettivamente cambia. Questo è l'output generato dal programma:

1,59142713593566 
1,59142713593566 
1,59142713593495 

Nota le differenze sugli ultimi 3 decimali. Questo non sembra una grande differenza, ma nel nostro codice di produzione i calcoli sono complessi e le differenze risultanti sono piuttosto grandi.

Non fa alcuna differenza se utilizzo il driver JET invece del driver ACE. Se cambio i tipi da doppio a decimale, l'errore scompare. Ma questa non è un'opzione nel nostro codice di produzione.

Sono in esecuzione su Windows 7 64 bit e gli assembly sono compilati per .NET 4.5 x86. L'utilizzo del driver ACE a 64 bit non è un'opzione in quanto viene eseguito Office a 32 bit.

Qualcuno sa perché questo sta accadendo e come posso risolvere il problema?

Il seguente codice riproduce il problema:

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

risposta

16

Questo è tecnicamente possibile, il codice non gestito può essere armeggiare con la parola di controllo FPU e cambiare il modo calcola. I noti produttori di problemi sono DLL compilati con gli strumenti Borland, il loro codice di supporto al runtime smaschera le eccezioni che possono danneggiare il codice gestito. E DirectX, è noto per armeggiare con la parola di controllo FPU per ottenere calcoli con doppio da eseguire come float per accelerare la matematica grafica.

Il tipo specifico di modifica della parola di controllo della FPU che appare qui è la modalità di arrotondamento, utilizzata dalla FPU quando è necessario scrivere un valore di registro interno con precisione di 80 bit in una posizione di memoria a 64 bit. Dispone di 4 opzioni per effettuare tale conversione: arrotondamento, arrotondamento, troncamento e round-to-even (arrotondamento del banchiere). Piccole differenze ma fai uno sforzo per accumularle rapidamente. E se il tuo modello numerico è instabile, sicuramente vedrai una differenza nel risultato finale. Questo non lo rende più o meno preciso, solo diverso.

Il codice gestito è piuttosto privo di difese contro il codice che esegue questa operazione, non è possibile accedere direttamente alla parola di controllo FPU. Richiede la scrittura del codice assembly. Hai un trucco disponibile, altamente non documentato ma piuttosto efficace. Il CLR sostituirà ogni volta che gestisce un'eccezione. Così si potrebbe fare questo:

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 fate attenzione che questo è costoso in modo da utilizzare il meno frequentemente possibile. Ed è una pita importante quando esegui il debug del codice, quindi potresti voler disabilitare questo nel build di Debug.

Vorrei menzionare un'alternativa, è possibile eseguire il pinvoke della funzione _fpreset() in msvcrt.dll.È comunque rischioso se lo si utilizza all'interno di un metodo che esegue anche operazioni matematiche in virgola mobile, l'ottimizzatore di jitter non sa che questa funzione fa tremare il tappetino. Avrete bisogno di testare a fondo la build di rilascio:

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

E non tenere a mente che questo non non rendere i risultati di calcolo più accurato in alcun modo. Solo diverso. Proprio come l'esecuzione della build Release del tuo codice senza un debugger produrrà risultati diversi rispetto alla build di Debug. Il codice di build Release eseguirà questo tipo di arrotondamento meno frequentemente poiché il jitter optimizer fa uno sforzo per mantenere i risultati intermedi all'interno della FPU con una precisione di 80 bit. Produrre un risultato diverso dalla build di Debug ma che in realtà è più accurato. Prendere o lasciare. Questo formato intermedio a 80 bit è stato l'errore di miliardi di dollari di Intel, non ripetuto nel set di istruzioni SSE2.

+0

Grazie per la tua risposta completa. Mi rendo conto che un risultato non è più corretto dell'altro, ma è un problema che il driver ACE ha questi effetti collaterali che causano cambiamenti nei calcoli fatti all'interno dello stesso processo. Ho provato i tuoi metodi e funzionano entrambi. Il metodo di eccezione funziona solo per le build di debug. Grazie per il tuo tempo. –

+0

Hmm, no, questo reset viene eseguito anche nella versione Release. Si trova all'interno del CLR, codice che non è influenzato dal modo in cui hai creato il tuo programma. Tenete presente l'ultimo paragrafo, otterrete risultati diversi nella versione di rilascio che dipendono dal fatto che sia stato collegato o meno un debugger. –

+0

Oh, mio ​​errore. Volevo dire che il metodo dell'eccezione funziona solo per le build di RELEASE, non per le build di debug. –