2013-06-04 33 views
5

Mam problem, gdy wydaje się, że wyniki niektórych obliczeń zmieniają się po użyciu Microsoft ACE driver w celu otwarcia arkusza kalkulacyjnego Excel.Sterownik Microsoft ACE zmienia precyzję zmiennoprzecinkową w pozostałej części mojego programu.

Kod poniżej przedstawia problem.

Pierwsze dwie rozmowy z numerem DoCalculation dają takie same wyniki. Następnie wywołuje funkcję OpenSpreadSheet, która otwiera i zamyka arkusz kalkulacyjny programu Excel 2003 przy użyciu sterownika ACE. Nie spodziewałbyś się, że OpenSpreadSheet będzie miał jakikolwiek wpływ na ostatnie połączenie z DoCalculation, ale okazuje się, że wynik faktycznie się zmienia. To jest dane wyjściowe generowane przez program:

1,59142713593566 
1,59142713593566 
1,59142713593495 

Uwaga różnice między ostatnimi 3 miejscami po przecinku. Nie wydaje się to dużą różnicą, ale w naszym kodzie produkcyjnym obliczenia są złożone, a wynikające różnice są dość duże.

Nie ma znaczenia, czy używam sterownika JET zamiast sterownika ACE. Jeśli zmienię typy z podwójnego na dziesiętny, błąd zniknie. Ale nie jest to opcja w naszym kodzie produkcyjnym.

Używam 64-bitowego systemu Windows 7, a zestawy są kompilowane dla .NET 4.5 x86. Używanie 64-bitowego sterownika ACE nie jest opcją, ponieważ używamy 32-bitowego pakietu Office.

Czy ktoś wie, dlaczego tak się dzieje i jak mogę to naprawić?

Następujący kod powtarza mój 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 
     } 
    } 
} 

Odpowiedz

16

Jest to technicznie możliwe, kod niekontrolowana można majstrować przy słowie sterującym FPU i zmienić sposób obliczania. Znanymi twórcami problemów są biblioteki DLL skompilowane z narzędziami Borland, a kod obsługi środowiska wykonawczego demaskuje wyjątki, które mogą spowodować awarię zarządzanego kodu. I DirectX, znany jest z majsterkowania ze słowem kontrolnym FPU, aby uzyskać obliczenia z podwójnym do wykonania jako float, aby przyspieszyć matematykę graficzną.

Specyficznym rodzajem zmiany słowa sterującego FPU, która wydaje się tu być, jest tryb zaokrąglania, używany przez jednostkę FPU, gdy musi wpisać wewnętrzną wartość rejestru z dokładnością 80-bitową do 64-bitowej lokalizacji pamięci. Ma 4 możliwości dokonania tej konwersji: zaokrąglić w górę, zaokrąglić w dół, skrócić i zaokrąglić do końca (zaokrąglenie bankiera). Bardzo małe różnice, ale starasz się je szybko zgromadzić. A jeśli twój model numeryczny jest niestabilny, na pewno zobaczysz różnicę w wyniku końcowym. To nie czyni go mniej lub bardziej dokładnym, po prostu innym.

Kod zarządzany jest dość bezbronny wobec kodu, który to robi, nie można uzyskać bezpośredniego dostępu do słowa kontrolnego FPU. Wymaga napisania kodu zespołu. Masz jedną dostępną sztuczkę, bardzo nieudokumentowaną, ale całkiem skuteczną. CLR będzie zresetować FPU za każdym razem, gdy obsługuje wyjątek. Możesz to zrobić:

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

Pamiętaj, że jest to kosztowne, więc używaj go tak rzadko, jak to możliwe. I jest to poważna pita podczas debugowania kodu, więc możesz chcieć wyłączyć to w kompilacji Debugowania.

Powinienem wymienić alternatywę, można wydzielić funkcję _fpreset() w msvcrt.dll.Jest jednak ryzykowne, jeśli używasz go w metodzie, która również wykonuje matematykę zmiennoprzecinkową, a optymalizator jittera nie wie, że ta funkcja szarpie matę podłogową. Musisz dokładnie przetestować kompilacji Release:

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

I nie pamiętać, że to robi nie dokonać Wyniki obliczeń bardziej precyzyjne w jakikolwiek sposób. Po prostu inny. Podobnie jak uruchamianie wersji Release twojego kodu bez debuggera będzie dawało inne wyniki niż kompilacja debugowania. Kod budowania wersji będzie rzadziej wykonywał tego rodzaju zaokrąglenia, ponieważ optymalizator jittera stara się utrzymać wyniki pośrednie wewnątrz jednostki FPU z dokładnością 80-bitową. Wytwarzanie innego wyniku z wersji debugowania, ale takiej, która jest bardziej dokładna. Daj albo bierz. Ten 80-bitowy format pośredni był pomyłką Intela w miliardach dolarów, nie powtórzoną w zestawie instrukcji SSE2.

+0

Dziękuję za dokładną odpowiedź. Rozumiem, że jeden wynik nie jest poprawniejszy od drugiego, ale jest problem, że sterownik ACE ma te skutki uboczne, które powodują zmiany w obliczeniach wykonanych w ramach tego samego procesu. Próbowałem twoich metod i obaj pracują. Metoda wyjątków działa jednak tylko w przypadku wersji debugowania. Dziękuję za Twój czas. –

+0

Hmm, nie, reset ten jest również wykonywany w wersji Release. Znajduje się wewnątrz CLR, kodu, na który nie ma wpływu sposób, w jaki zbudowałeś swój program. Czy pamiętasz ostatni akapit, otrzymasz różne wyniki w wersji Release, które zależą od tego, czy masz dołączony debugger. –

+0

Och, mój błąd. Chciałem powiedzieć, że metoda exception działa tylko w przypadku RELEASE builds - nie debugowania kompilacji. –