Драйвер Microsoft ACE изменяет точность с плавающей запятой в остальной части моей программы
У меня возникла проблема, когда кажется, что результаты некоторых вычислений меняются после использования драйвера Microsoft ACE для открытия электронной таблицы Excel.
Код поддерживает воспроизведение проблемы.
Первые два вызова DoCalculation
дают одинаковые результаты. Затем я вызываю функцию OpenSpreadSheet
которая открывает и закрывает электронную таблицу Excel 2003 с использованием драйвера ACE. Вы не ожидали, что OpenSpreadSheet
окажет какое-то влияние на последний вызов DoCalculation
но окажется, что результат действительно изменится. Это результат, который генерирует программа:
- Печать двойная без потери точности
- В чем разница между float и double?
- Как напечатать двойное значение с полной точностью с помощью cout?
- Безопасно ли проверять значения с плавающей запятой на равенство 0?
- Печать чисел с плавающей запятой из x86-64, по-видимому, требует сохранения% rbp
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 } } }
- Как представить 0,1 в арифметике с плавающей точкой и десятичной запятой
- Изменение режима округления с плавающей запятой
- Проверка, является ли float целым числом
- Разделение целых чисел на Java
- Проблема с поплавками в Objective-C
- Суффикс «f» по плавающей стоимости?
- Плавающая точка и целочисленные вычисления на современном оборудовании
- (.1f + .2f ==. 3f)! = (.1f + .2f) .Equals (.3f) Почему?
Это технически возможно, неуправляемый код может манипулировать управляющим словом 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.