Драйвер 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
}
}
}
1 ответ
Это технически возможно, неуправляемый код может повозиться с управляющим словом FPU и изменить способ его вычисления. Хорошо известными источниками проблем являются библиотеки DLL, скомпилированные с помощью инструментов Borland, их код поддержки во время выполнения разоблачает исключения, которые могут привести к сбою управляемого кода. А DirectX известен тем, что манипулирует с управляющим словом FPU, чтобы получить вычисления с двойным числом, которые будут выполняться как 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) {}
}
Имейте в виду, что это дорого, поэтому используйте его как можно реже. И это очень важно, когда вы отлаживаете код, поэтому вы можете отключить его в сборке Debug.
Я должен упомянуть альтернативу, вы можете вызвать функцию _fpreset() в msvcrt.dll. Однако рискованно, если вы используете его внутри метода, который также выполняет математику с плавающей запятой, оптимизатор джиттера не знает, что эта функция дергает коврик. Вам нужно будет тщательно протестировать сборку Release:
[System.Runtime.InteropServices.DllImport("msvcrt.dll")]
public static extern void _fpreset();
И имейте в виду, что это не сделает ваши результаты расчетов более точными. Просто другой. Точно так же, как запуск сборки Release вашего кода без отладчика, даст результаты, отличные от сборки Debug. Код сборки Release будет выполнять такое округление реже, поскольку оптимизатор джиттера старается сохранять промежуточные результаты внутри FPU с 80-битной точностью. Получение результата, отличного от сборки Debug, но более точного. Дай или возьми. Этот 80-битный промежуточный формат был ошибкой Intel на миллиард долларов, и он не повторялся в наборе инструкций SSE2.