2013-06-04 5 views
5

Microsoft ACE driver을 사용하여 Excel 스프레드 시트를 연 후 일부 계산 결과가 변경되는 것으로 보이는 문제가 있습니다.Microsoft ACE 드라이버가 나머지 프로그램에서 부동 소수점 정밀도를 변경합니다.

코드는 문제를 재현합니다.

DoCalculation에 대한 처음 두 건은 동일한 결과를 산출합니다. 그런 다음 ACS 드라이버를 사용하여 Excel 2003 스프레드 시트를 열고 닫는 OpenSpreadSheet 함수를 호출합니다. DoCalculation에 대한 마지막 호출에 OpenSpreadSheet이 아무런 영향을 미치지 않을 것으로 예상되지만 결과는 실제로 변경됩니다. 프로그램에서 생성하는 출력은 다음과 같습니다.

1,59142713593566 
1,59142713593566 
1,59142713593495 

마지막 3 자리수의 차이점에 유의하십시오. 이것은 큰 차이점처럼 보이지는 않지만 생산 코드에서 계산은 복잡하며 결과 차이는 상당히 큽니다.

ACE 드라이버 대신 JET 드라이버를 사용하면 아무런 차이가 없습니다. 형식을 double에서 decimal로 변경하면 오류가 사라집니다. 그러나 이것은 우리 프로덕션 코드에서 옵션이 아닙니다.

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

답변

16

, 관리되지 않는 코드가 FPU 제어 단어로 땜질 할 수있다을하고 계산하는 방식을 변경합니다. 잘 알려진 문제 해결 업체는 Borland 도구로 컴파일 된 DLL이며, 런타임 지원 코드는 관리 코드를 손상시킬 수있는 예외를 언 마스킹합니다. 그리고 DirectX에서는 FPU 제어 단어를 사용하여 double의 계산을 float으로 수행하여 그래픽 수학 속도를 높이는 것으로 알려져 있습니다.

FPU 제어 워드 변경의 특정 종류는 반올림 모드로, FPU가 80 비트 정밀도의 내부 레지스터 값을 64 비트 메모리 위치에 쓸 필요가있을 때 사용합니다. 변환을 수행하는 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) {} 
} 

이 너무 가능한 한 자주 사용하는 비싼 것을 조심 마십시오. 디버그 빌드에서 코드를 디버깅 할 때 코드를 디버깅 할 때 중요한 피타입니다.

나는 대안을 언급해야한다. msvcrt.dll에서 _fpreset() 함수를 pinvoke 할 수있다.그러나 부동 소수점 연산을 수행하는 메소드 내부에서 사용하면 위험합니다. 지터 최적화 프로그램은이 함수가 바닥 매트를 경련시키는 지 알지 못합니다.

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

을 그리고이 어떤 방식으로 계산 결과가 더 정확하지 않는다는 것을 명심 않은 : 당신은 철저하게 릴리스 빌드를 테스트해야합니다. 그냥 다르다. 디버거없이 코드의 릴리스 빌드를 실행하면 디버그 빌드와 다른 결과가 생성됩니다. 지터 최적화 기가 FPU 내부에서 중간 결과를 80 비트 정밀도로 유지하기 위해 노력하므로 릴리스 빌드 코드는이 종류의 반올림을 덜 수행합니다. 디버그 빌드와는 다른 결과를 생성하지만 실제로는 더 정확합니다. 주고받습니다. 이 80 비트 중간 형식은 SSE2 명령어 세트에서 반복되지 않은 인텔의 10 억 달러 실수입니다.

+0

철저한 답변 해 주셔서 감사합니다. 한 가지 결과가 다른 것보다 정확하지 않다는 것을 알고 있지만 ACE 드라이버가 동일한 프로세스 내에서 계산을 변경하는 이러한 부작용을 가지고 있다는 것이 문제입니다. 나는 당신의 방법을 시도하고 그들은 둘 다 작동합니다. 예외 메서드는 디버그 빌드에서만 작동합니다. 시간 내 주셔서 감사합니다. –

+0

흠, 아니요,이 재설정은 릴리스 빌드에서도 수행됩니다. 이 코드는 CLR 내부에 있으며 프로그램을 작성한 방식에 영향을받지 않습니다. 마지막 단락을 명심하십시오. 디버거가 연결되어 있는지 여부에 따라 릴리스 빌드에서 다른 결과가 나옵니다. –

+0

오, 내 실수. exeption 메서드는 RELEASE 빌드에서만 작동합니다. 디버그 빌드가 아닙니다. –