Драйвер Microsoft ACE изменяет точность с плавающей запятой в остальной части моей программы

У меня возникла проблема, когда кажется, что результаты некоторых вычислений меняются после использования драйвера Microsoft ACE для открытия электронной таблицы Excel.

Код поддерживает воспроизведение проблемы.

Первые два вызова DoCalculation дают одинаковые результаты. Затем я вызываю функцию OpenSpreadSheet которая открывает и закрывает электронную таблицу Excel 2003 с использованием драйвера ACE. Вы не ожидали, что OpenSpreadSheet окажет какое-то влияние на последний вызов DoCalculation но окажется, что результат действительно изменится. Это результат, который генерирует программа:

 1,59142713593566 1,59142713593566 1,59142713593495 

Обратите внимание на различия в последних 3 десятичных знаках. Это не похоже на большую разницу, но в нашем производственном коде вычисления сложны, и полученные различия довольно велики.

Не имеет значения, если я использую драйвер JET вместо драйвера ACE. Если я изменю типы от двойного до десятичного, ошибка исчезнет. Но это не вариант нашего производственного кода.

Я работаю на 64-разрядной версии Windows 7, а сборки собираются для .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 } } } 

Это технически возможно, неуправляемый код может манипулировать управляющим словом FPU и изменять способ его вычисления. Известными разработчиками проблем являются библиотеки DLL, скомпилированные с инструментами Borland, их код поддержки во время выполнения маскирует исключения, которые могут привести к сбою управляемого кода. И DirectX, как известно, возится с управляющим словом FPU, чтобы получить вычисления с двойным значением, которое должно выполняться как float для ускорения графической математики.

Определенный вид изменения слов управления FPU, который, как представляется, сделан здесь, – это режим округления, используемый FPU, когда ему нужно записать значение внутреннего регистра с 80-битной точностью в 64-разрядную ячейку памяти. У него есть 4 варианта для преобразования: округление, округление, усечение и округление до четности (округление банкира). Очень небольшие различия, но вы делаете попытку аккумулировать их быстро. И если ваша численная модель нестабильна, вы наверняка увидите разницу в конечном результате. Это не делает его более или менее точным, просто другим.

Управляемый код довольно беззащитен для кода, который делает это, вы не можете напрямую обращаться к управляющему слову FPU. Это требует написания кода сборки. У вас есть один ansible трюк, очень недокументированный, но довольно эффективный. 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) {} } 

Остерегайтесь, что это дорого, поэтому используйте как можно реже. И это крупная пита, когда вы отлаживаете код, поэтому вы можете отключить его в сборке Debug.

Я должен упомянуть альтернативу, вы можете вывести функцию _fpreset () в msvcrt.dll. Однако рискованно, если вы используете его внутри метода, который также выполняет математику с плавающей запятой, оптимизатор дрожания не знает, что эта функция подталкивает коврик. Вам необходимо тщательно протестировать сборку Release:

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

И имейте в виду, что это не делает ваши расчеты более точными. Просто другой. Точно так же, как запуск сборки Release вашего кода без отладчика приведет к различным результатам, чем assembly Debug. Код сборки Release будет выполнять этот вид округления реже, поскольку оптимизатор джиттера делает попытку сохранить промежуточные результаты внутри FPU с точностью до 80 бит. Производя другой результат из сборки Debug, но тот, который на самом деле более точен. Дай или возьми. Этот 80-битный промежуточный формат был ошибкой миллиарда долларов Intel, а не повторялся в наборе инструкций SSE2.

  • плавать до неожиданного поведения
  • Как представить номер FLOAT в памяти на C
  • Спецификатор ширины печати для поддержания точности значения с плавающей запятой
  • «плавающая» и «двойная» точность
  • Сравнение чисел с плавающей запятой в C
  • разбиение числа на целые и десятичные числа
  • C ++: как округлить двойной к int?
  • Преобразование из строки научной нотации в float в C #
  • Какие операции и функции на +0.0 и -0.0 дают разные арифметические результаты?
  • Самый быстрый способ сделать горизонтальную векторную сумму float на x86
  • Как числа с плавающей запятой сохраняются в памяти?
  • Давайте будем гением компьютера.