KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » А. Григорьев - О чём не пишут в книгах по Delphi

А. Григорьев - О чём не пишут в книгах по Delphi

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн А. Григорьев, "О чём не пишут в книгах по Delphi" бесплатно, без регистрации.
Перейти на страницу:

В том, что описанная проблема с потерей точности встречается все реже, есть заслуга и разработчиков VCL. Зная, вызовы каких функций могут привести к изменению управляющего слова FPU, они перед этими вызовами запоминают управляющее слово, а затем восстанавливают. В более поздних версиях Delphi количество таких "оберток" больше, чем в ранних, поэтому чем новее версия Delphi, тем меньше шанс столкнуться с описанной проблемой. Здесь мы рассмотрим несколько примеров из исходного кода стандартных модулей Delphi 2007.

Для динамической загрузки DLL предназначена API-функция LoadLibrary. В модуле SysUtils для этой функции предлагается обертка, называющаяся SafeLoadLibrary (листинг 3.13).

Листинг 3.13. Функция SysUtils.SafeLoadLibrary

{ SafeLoadLibrary calls LoadLibrary, disabling normal Win32 error message popup dialogs if the requested file can't be loaded. SafeLoadLibrary also preserves the current FPU control word (precision, exception masks) across the LoadLibrary call (in case the DLL you're loading hammers the FPU control word in its initialization, as many MS DLLs do) }

function SafeLoadLibrary(const Filename: string; ErrorMode: UINT): HMODULE;

var

 OldMode: UINT;

 FPUControlWord: Word;

begin

 OldMode := SetErrorMode(ErrorMode); 

 try

  asm

   FNSTCW FPUControlWord

  end;

  try

   Result := LoadLibrary(PChar(Filename));

  finally

   asm

    FNCLEX

    FLDCW FPUControlWord

   end;

  end;

 finally

  SetErrorMode(OldMode);

 end;

end;

Как видно из комментария, проблема в том, что многие системные библиотеки изменяют управляющее слово FPU при своей инициализации.

В функции CreateADOObject (внутренняя функция модуля ADODB) тоже сохраняется и восстанавливается управляющее слово (листинг 3.14).

Листинг 3.14. Функция CreateADOObject модуля ADODB

function CreateADOObject(const ClassID: TGUID): IUnknown;

var

 Status: HResult;

 FPUControlWord: Word;

begin

 asm

  FNSTCW FPUControlWord

 end;

 Status :=

  CoCreateInstance(ClassID, nil, CLSTX_INPROC_SERVER or CLSCTX_LOCAL_SERVER, IUnknown, Result);

 asm

  FNCLEX

  FLDCW FPUControlWord

 end;

 if (Status = REGDB_E_CLASSNOTREG) then

  raise Exception.CreateRes(@SADOCreateError)

 else OleCheck(Status);

end;

Здесь восстанавливать управляющее слово приходится после вызова системной функции CoCreateInstance, создающей СОМ-объект. Но, судя по тому, что больше нигде при вызове CoCreateInstance такой код не используется, проблема не в самой функции, а в тех конкретных ADO-объектах, которые создаются здесь с ее помощью.

Аналогичную защиту можно обнаружить в модуле Dialogs, в методе TCommonDialog.TaskModalDialog. Комментарий к этой защите гласит: "Avoid FPU control word change in NETRAP.dll, NETAPI32.dll, etc".

В модуле Windows особым образом импортируются функции CreateWindow и CreateWindowEx, которые, видимо, тоже были замечены в некорректном обращении с управляющим словом FPU. Вот как, например, выглядит импорт функции CreateWindowEx (листинг 3.15).

Листинг 3.15. Импорт функции CreateWindowEx модулем Windows

function _CreateWindowEx(dwExStyle: WORD; lpClassName: PChar; lpWindowName: PChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall; external user32 name 'CreateWindowExA';


function CreateWindowEx(dwExStyle: DWORD; lpClassName: PChar; lpWindowName: PChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND;

var

 FPUCW: Word;

begin

 FPUCW := Get8087CW;

 Result :=

  _CreateWindowEx(dwExStyle, lpClassName, lpWindowName,

  dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu,

  hInstance, lpParam);

 Set8087CW(FPUCW);

end;

Модуль Windows импортирует функцию CreateWindowExA из библиотеки user32.dll, но дает ей измененное название и не показывает ее в своем интерфейсе. Вместо этого он экспортирует другую функцию с названием CreateWindowEx (и аналогичную с названием CreateWindowExA), которая является оберткой над настоящей CreateWindowExA и обеспечивает сохранение значения управляющего слова FPU. Аналогичным способом импортируется и Unicode-вариант функции. Таким образом, стандартные библиотеки обеспечивают вызов безопасного варианта CreateWindowEx в любой программе.

Примечание

В модуле Windows можно обнаружить еще одну интересную деталь: функции CreateWindowA и CreateWindowW из библиотеки user32.dll этим модулем вообще не импортируются. Вместо этого одноименные обертки вызывают импортированные функции _CreateWindowExA и _CreateWindowExW, передавая им 0 в качестве значения параметра dwExStyle.

3.2.12. Машинное эпсилон

Когда мы имеем дело с вычислениями с ограниченной точностью, возникает такой парадокс. Пусть, например, мы считаем с точностью до трех значащих цифр. Прибавим к числу 1,00 число 1,00·10-4. Если бы все было честно, мы получили бы 1,0001. Но у нас ограничена точность, поэтому мы вынуждены округлять до трех значащих цифр. В результате получается 1,00. Другими словами, к некоторому числу мы прибавляем другое число, большее нуля, а в результате из-за ограниченной точности мы получаем то же самое число. Наименьшее положительное число, которое при добавлении его к единице дает результат, не равный единице, называется машинным эпсилон.

Понятие машинного эпсилон у новичков нередко путается с понятием наименьшего числа, которое может быть записано в выбранном формате. Это неправильно. Машинное эпсилон определяется только размером мантиссы, а минимально возможное число оказывается существенно меньше из-за сдвига плавающей двоичной точки с помощью экспоненты.

Прежде чем искать машинное эпсилон программно, попытаемся найти его из теоретических соображений. Итак, мантисса типа Extended содержит 64 разряда. Чтобы закодировать единицу, старший бит мантиссы должен быть равен 1 (денормализованная запись), остальные биты — нулю. Очевидно, что при такой записи наименьшее из чисел, для которых выполняется условие x > 1, получается, когда самый младший бит мантиссы тоже будет равен единице, т.е. х = 1,00...001 (в двоичном представлении, между точкой и младшей единицей 62 нуля). Таким образом, машинное эпсилон равно х-1, т.е. 0.00...001. В более привычной десятичной форме записи это будет 2-63, т.е. примерно 1,084·10-19.

Листинг 3.16 показывает, как можно найти это число (пример Epsilon на компакт-диске).

Листинг 3.16. Поиск машинного эпсилон

procedure TForm1.Button1Click(Sender: TObject);

var

 R: Extended;

 I: Integer;

begin

 R := 1;

 while 1 + R/2 > 1 do R := R / 2;

 Label1.Caption := FloatToStr(R);

end;

Запустив этот код, мы получим на экране 1.0842021724855Е-19 в полном соответствии с нашими теоретическими выкладками.

Примечание

В тех системах, где наблюдается описанная проблема с уменьшением точности, программа выдаст 2.22044604925031Е-16. Если вы увидели у себя это число, добавьте код, который переведет FPU в режим максимальной точности.

А теперь изменим тип переменной R с Extended на Double. Результат не изменится. На Single — опять не изменится. Но такое поведение лишь на первый взгляд может показаться странным. Давайте подробнее рассмотрим выражение 1 + R / 2 > 1. Итак, все вычисления (в том числе и сравнение) сопроцессор выполняет с данными типа Extended. Последовательность действий такова: число R загружается в регистр сопроцессора, преобразуясь при этом к типу Extended. Дальше оно делится на 2, а затем к результату прибавляется 1, и все это в Extended, никакого обратного преобразования в Single или Double не происходит. Затем это число сравнивается с единицей. Очевидно, что результат сравнения не должен зависеть от исходного типа R, т.к. диапазона даже типа Single вполне хватает, чтобы разместить машинное эпсилон.

3.2.13. Методы решения проблем

Подведем итоги сказанному. Значения, которые мы получаем, могут отличаться от ожидаемых, даже если речь идет о простом присваивании. Во многих случаях (например, в научных расчетах) это несущественно, т.к. сам метод расчета дает еще большую погрешность. Проблемы начинаются там, где мы хотим вывести число на экран или сравнить его с другим. Универсальных рецептов на все случаи жизни не существует, но во многих ситуациях помогают следующие советы:

□ Если ваша задача — просто получить "красивое" представление числа на экране, то функцию FloatToStr заменяйте на ее более мощный аналог FloatToStrF или на функцию Format — они позволяют указать желаемое количество символов после точки.

□ Сравнение вещественных чисел следует выполнять с учетом погрешности, т.е. вместо if а = b … писать if Abs(а - b) < Ерs …, где Eps — некоторая величина, задающая допустимую погрешность (в модуле Math, начиная с Delphi 6, существует функция SameValue, с помощью которой это же условие можно записать как if SameValue(a, b, Eps) …).

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*