7

Microsoft ACE ドライバーを使用して Excel スプレッドシートを開いた後、一部の計算結果が変更されたように見えるという問題が発生しています。

以下のコードは、問題を再現します。

最初の 2 つの呼び出しDoCalculationで同じ結果が得られます。OpenSpreadSheet次に、ACE ドライバーを使用して Excel 2003 スプレッドシートを開いたり閉じたりする関数を呼び出します。OpenSpreadSheetへの最後の呼び出しに影響があるとは思わないでしょうがDoCalculation、実際には結果が変わることがわかります。これは、プログラムが生成する出力です。

1,59142713593566
1,59142713593566
1,59142713593495

小数点以下 3 桁の違いに注意してください。これは大きな違いのようには見えませんが、私たちの製品コードでは計算が複雑で、結果の違いは非常に大きくなります。

ACE ドライバーの代わりに JET ドライバーを使用しても違いはありません。タイプをdoubleからdecimalに変更すると、エラーはなくなります。しかし、これは製品コードのオプションではありません。

Windows 7 64 ビットで実行しており、アセンブリは .NET 4.5 x86 用にコンパイルされています。32 ビットの Office を実行しているため、64 ビットの ACE ドライバーを使用することはできません。

なぜこれが起こっているのか、どうすれば修正できるのか誰か知っていますか?

次のコードは私の問題を再現します:

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

1 に答える 1

17

これは技術的に可能です。アンマネージ コードが FPU コントロール ワードをいじり、計算方法を変更している可能性があります。よく知られているトラブル メーカーは、Borland ツールでコンパイルされた DLL であり、そのランタイム サポート コードは、マネージ コードをクラッシュさせる可能性のある例外のマスクを解除します。そして DirectX は、FPU コントロール ワードをいじってdoubleの計算をfloatとして実行し、グラフィック演算を高速化することで知られています。

ここで行われるように見える特定の種類の FPU 制御ワードの変更は、丸めモードであり、80 ビット精度の内部レジスタ値を 64 ビットのメモリ位置に書き込む必要があるときに FPU によって使用されます。その変換を行うには、切り上げ、切り捨て、切り捨て、偶数への丸め (バンカーの丸め) の 4 つのオプションがあります。非常に小さな違いですが、それらを急速に蓄積する努力をしています。また、数値モデルが不安定な場合は、最終結果に確実に違いが見られます。それは多かれ少なかれ正確になるわけではなく、単に異なるだけです。

マネージ コードは、これを行うコードに対してかなり無防備です。FPU コントロール ワードに直接アクセスすることはできません。アセンブリ コードを記述する必要があります。非常に文書化されていませんが、非常に効果的なトリックが 1 つあります。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() 関数を呼び出すことができます。ただし、浮動小数点演算も実行するメソッド内で使用すると危険です。ジッター オプティマイザーは、この関数がフロア マットをジャークすることを認識しません。リリース ビルドを徹底的にテストする必要があります。

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

また、これによって計算結果がより正確になるわけではないことに注意してください。ただ違う。デバッガーなしでコードのリリース ビルドを実行すると、デバッグ ビルドとは異なる結果が生成されるのと同じように。リリース ビルド コードは、ジッター オプティマイザーが 80 ビット精度で FPU 内の中間結果を保持しようとするため、この種の丸めをあまり頻繁に実行しません。Debug ビルドとは異なる結果を生成しますが、実際にはより正確です。ギブオアテイク。この 80 ビットの中間形式は、SSE2 命令セットで繰り返されていない Intel の 10 億ドルの間違いでした。

于 2013-06-04T12:13:09.360 に答える