Неверная операция с плавающей запятой, вызывающая Trunc()
Я получаю (повторяемое) исключение с плавающей запятой, когда я пытаюсь Trunc()
a Real
.
например:.
Trunc(1470724508.0318);
В действительности реальный код более сложный:
ns: Real;
v: Int64;
ns := ((HighPerformanceTickCount*1.0)/g_HighResolutionTimerFrequency) * 1000000000;
v := Trunc(ns);
Но в итоге все еще сводится к:
Trunc(ARealValue);
Теперь я не могу повторять его нигде - просто на этом месте. Где он терпит неудачу каждый раз.
Это не voodoo
К счастью, компьютеры не волшебны. Процессор Intel выполняет очень конкретные наблюдаемые действия. Поэтому я должен понять, почему операция с плавающей запятой терпит неудачу.
Переход в окно CPU
v: = Trunc (ns)
fld qword ptr [ebp-$10]
Это загружает 8-байтовое значение с плавающей запятой в ebp- $10 в регистр с плавающей запятой ST0
.
Байт по адресу памяти [ebp- $10]:
0018E9D0: 6702098C 41D5EA5E (as DWords)
0018E9D0: 41D5EA5E6702098C (as QWords)
0018E9D0: 1470724508.0318 (as Doubles)
Вызов успешно завершен, а регистр с плавающей запятой содержит соответствующее значение:
![enter image description here]()
Далее - это фактический вызов функции RTL Trunc:
call @TRUNC
Далее следует мужество функции Delphi RTL Trunc:
@TRUNC:
sub esp,$0c
wait
fstcw word ptr [esp] //Store Floating-Point Control Word on the stack
wait
fldcw word ptr [cwChop] //Load Floating-Point Control Word
fistp qword ptr [esp+$04] //Converts value in ST0 to signed integer
//stores the result in the destination operand
//and pops the stack (increments the stack pointer)
wait
fldcw word ptr [esp] //Load Floating-Point Control Word
pop ecx
pop eax
pop edx
ret
Или я полагаю, что мог бы просто вставить его из rtl, а не расшифровать его из окна CPU:
const cwChop : Word = $1F32;
procedure _TRUNC;
asm
{ -> FST(0) Extended argument }
{ <- EDX:EAX Result }
SUB ESP,12
FSTCW [ESP] //Store foating-control word in ESP
FWAIT
FLDCW cwChop //Load new control word $1F32
FISTP qword ptr [ESP+4] //Convert ST0 to int, store in ESP+4, and pop the stack
FWAIT
FLDCW [ESP] //restore the FPCW
POP ECX
POP EAX
POP EDX
end;
Исключение происходит во время фактической операции fistp.
fistp qword ptr [esp+$04]
В момент этого вызова регистр ST0 будет содержать одно и то же значение с плавающей запятой:
![enter image description here]()
Примечание: внимательный наблюдатель заметит, что значение в приведенном выше скриншоте не соответствует первому снимку экрана. Это потому, что я взял его в другой перспективе. Я бы предпочел не переустанавливать все константы в вопросе, чтобы сделать их согласованными, но поверьте мне: это то же самое, когда я достигаю инструкции fistp
, как это было после инструкции fld
.
Достижение этого:
-
sub esp,$0c
: Я смотрю, как он сдвигает стек на 12 байт.
-
fstcw word ptr [esp]
: я смотрю, как он нажимает $027F в указатель текущего стека
-
fldcw word ptr [cwChop]
: я наблюдаю за изменением флагов с плавающей запятой
-
fistp qword ptr [esp+$04]
: и он собирается написать Int64 в комнату, которую он сделал в стеке
а затем он сработает.
Что на самом деле происходит здесь?
Это происходит и с другими значениями, но это не похоже на что-то не так с этим конкретным значением с плавающей запятой. Но я даже пытался настроить тестовый сценарий в другом месте.
Зная, что 8-байтовое шестнадцатеричное значение поплавка: $41D5EA5E6702098C
, я попытался выполнить настройку:
var
ns: Real;
nsOverlay: Int64 absolute ns;
v: Int64;
begin
nsOverlay := $41d62866a2f270dc;
v := Trunc(ns);
end;
Что дает:
nsOverlay: = $41d62866a2f270dc;
mov [ebp-$08],$a2f270dc
mov [ebp-$04],$41d62866
v: = Trunc (ns)
fld qword ptr [ebp-$08]
call @TRUNC
И в точке call
до @trunc
регистр ST0 с плавающей запятой содержит значение:
![enter image description here]()
Однако вызов не завершается с ошибкой. Он только терпит неудачу, каждый раз в этом одном разделе моего кода.
Что может произойти, что заставляет процессор бросать invalid floating point exception
?
Каково значение cwChop
перед загрузкой управляющего слова?
Значение cwChop
выглядит корректно перед словом управления нагрузкой $1F32
. Но после загрузки управляющее слово фактическое неверно:
![enter image description here]()
Бонус Chatter
Фактическая функция, которая терпит неудачу, - это то, что позволяет конвертировать высокопроизводительные отметки тиков в наносекунды:
function PerformanceTicksToNs(const HighPerformanceTickCount: Int64): Int64;
//Convert high-performance ticks into nanoseconds
var
ns: Real;
v: Int64;
begin
Result := 0;
if HighPerformanceTickCount = 0 then
Exit;
if g_HighResolutionTimerFrequency = 0 then
Exit;
ns := ((HighPerformanceTickCount*1.0)/g_HighResolutionTimerFrequency) * 1000000000;
v := Trunc(ns);
Result := v;
end;
Я создал все временные переменные intermeidate, чтобы попытаться отследить, где произошел сбой.
Я даже пытался использовать это как шаблон, чтобы попытаться воспроизвести его:
var
i1, i2: Int64;
ns: Real;
v: Int64;
vOver: Int64 absolute ns;
begin
i1 := 5060170;
i2 := 3429541;
ns := ((i1*1.0)/i2) * 1000000000;
//vOver := $41d62866a2f270dc;
v := Trunc(ns);
Но он отлично работает. Там что-то о том, когда он вызывал во время DUnit unit test.
Флаги управляющих слов с плавающей запятой
Стандартное слово управления Delphi: $1332
:
$1332 = 0001 00 11 00 110010
0 ;Don't allow invalid numbers
1 ;Allow denormals (very small numbers)
0 ;Don't allow divide by zero
0 ;Don't allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
0 ;reserved exception mask
0 ;reserved
11 ;Precision Control - 11B (Double Extended Precision - 64 bits)
00 ;Rounding control -
0 ;Infinity control - 0 (not used)
Требуемое значение для Windows API: $027F
$027F = 0000 00 10 01 111111
1 ;Allow invalid numbers
1 ;Allow denormals (very small numbers)
1 ;Allow divide by zero
1 ;Allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
1 ;reserved exception mask
0 ;reserved
10 ;Precision Control - 10B (double precision)
00 ;Rounding control
0 ;Infinity control - 0 (not used)
Управляющее слово crChop
: $1F32
$1F32 = 0001 11 11 00 110010
0 ;Don't allow invalid numbers
1 ;Allow denormals (very small numbers)
0 ;Don't allow divide by zero
0 ;Don't allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
0 ;reserved exception mask
0 ;unused
11 ;Precision Control - 11B (Double Extended Precision - 64 bits)
11 ;Rounding Control
1 ;Infinity control - 1 (not used)
000 ;unused
Флаги CTRL
после загрузки $1F32
: $1F72
$1F72 = 0001 11 11 01 110010
0 ;Don't allow invalid numbers
1 ;Allow denormals (very small numbers)
0 ;Don't allow divide by zero
0 ;Don't allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
1 ;reserved exception mask
0 ;unused
11 ;Precision Control - 11B (Double Extended Precision - 64 bits)
11 ;Rounding control
1 ;Infinity control - 1 (not used)
00011 ;unused
Все, что делает процессор, это включение зарезервированного неиспользуемого бита маски.
RaiseLastFloatingPointError()
Если вы собираетесь разрабатывать программы для Windows, вам действительно нужно принять тот факт, что исключения с плавающей запятой должны быть замаскированы процессором, а это значит, что вы должны следить за ними самостоятельно. Подобно Win32Check
или RaiseLastWin32Error
, нам нужно a RaiseLastFPError
. Лучшее, что я могу придумать, это:
procedure RaiseLastFPError();
var
statWord: Word;
const
ERROR_InvalidOperation = $01;
// ERROR_Denormalized = $02;
ERROR_ZeroDivide = $04;
ERROR_Overflow = $08;
// ERROR_Underflow = $10;
// ERROR_InexactResult = $20;
begin
{
Excellent reference of all the floating point instructions.
(Intel architecture manuals have no organization whatsoever)
http://www.plantation-productions.com/Webster/www.artofasm.com/Linux/HTML/RealArithmetica2.html
Bits 0:5 are exception flags (Mask = $2F)
0: Invalid Operation
1: Denormalized - CPU handles correctly without a problem. Do not throw
2: Zero Divide
3: Overflow
4: Underflow - CPU handles as you'd expect. Do not throw.
5: Precision - Extraordinarily common. CPU does what you'd want. Do not throw
}
asm
fwait //Wait for pending operations
FSTSW statWord //Store floating point flags in AX.
//Waits for pending operations. (Use FNSTSW AX to not wait.)
fclex //clear all exception bits the stack fault bit,
//and the busy flag in the FPU status register
end;
if (statWord and $0D) <> 0 then
begin
//if (statWord and ERROR_InexactResult) <> 0 then raise EInexactResult.Create(SInexactResult)
//else if (statWord and ERROR_Underflow) <> 0 then raise EUnderflow.Create(SUnderflow)}
if (statWord and ERROR_Overflow) <> 0 then raise EOverflow.Create(SOverflow)
else if (statWord and ERROR_ZeroDivide) <> 0 then raise EZeroDivide.Create(SZeroDivide)
//else if (statWord and ERROR_Denormalized) <> 0 then raise EUnderflow.Create(SUnderflow)
else if (statWord and ERROR_InvalidOperation) <> 0 then raise EInvalidOp.Create(SInvalidOp);
end;
end;
Воспроизводимый случай!
Я нашел случай, когда слово управления положением с плавающей запятой по умолчанию Delphi было причиной недопустимого исключения с плавающей запятой (хотя я никогда не видел его раньше, потому что он был замаскирован). Теперь, когда я это вижу, почему это происходит! И это воспроизводимо:
procedure TForm1.Button1Click(Sender: TObject);
var
d: Real;
dover: Int64 absolute d;
begin
d := 1.35715152325557E020;
// dOver := $441d6db44ff62b68; //1.35715152325557E020
d := Round(d); //<--floating point exception
Self.Caption := FloatToStr(d);
end;
Вы можете видеть, что регистр ST0
содержит допустимое значение с плавающей запятой. Слово управления с плавающей запятой $1372
. Флаг исключения с плавающей запятой все ясен:
![enter image description here]()
И затем, как только он будет выполнен, это приведет к недействительной операции:
![enter image description here]()
-
IE
(Неверная операция) установлен флаг
Установлен флаг -
ES
(Исключение)
У меня возникло соблазн задать этот вопрос как другой вопрос, но это был бы тот же самый вопрос, за исключением того, что на этот раз вызывал Round()
.
Ответы
Ответ 1
Проблема возникает в другом месте. Когда ваш код вводится Trunc
, для управляющего слова установлено значение $027F
, которое является IIRC, стандартным словосочетанием Windows. Все это замаскировано. Это проблема, потому что Delphi RTL ожидает, что исключения будут разоблачены.
И посмотрите на окно FPU, наверняка есть ошибки. И IE, и PE флаги установлены. Это IE, который имеет значение. Это означает, что ранее в кодовой последовательности была скрытая недействительная операция.
Затем вы вызываете Trunc
, который модифицирует управляющее слово, чтобы разоблачить исключения. Посмотрите на свой второй скриншот окна FPU. IE равен 1, но IM равно 0. Так что бум, более раннее исключение поднято, и вы вынуждены думать, что это была ошибка Trunc
. Это не так.
Вам нужно будет проследить резервную копию стека вызовов, чтобы узнать, почему контрольное слово не то, что должно быть в программе Delphi. Это должно быть $1332
. Скорее всего, вы звоните в стороннюю библиотеку, которая модифицирует управляющее слово и не восстанавливает его. Вам нужно будет найти виновника и взять на себя ответственность при каждом вызове этой функции.
Как только вы вернете контрольное слово под контроль, вы найдете настоящую причину этого исключения. Очевидно, что существует незаконная операция FP. После того, как управляющее слово развяжет исключения, ошибка будет поднята в правой точке.
Обратите внимание, что нечего беспокоиться о несоответствии между $1372
и $1332
, или $1F72
и $1F32
. Это просто странность с управляющим словом CTRL
, что некоторые из байтов зарезервированы и игнорируют ваши увещания, чтобы очистить их.
Ответ 2
Ваше последнее обновление по существу задает другой вопрос. Он спрашивает об исключении, вызванном этим кодом:
procedure foo;
var
d: Real;
i: Int64;
begin
d := 1.35715152325557E020;
i := Round(d);
end;
Этот код терпит неудачу, потому что задание Round()
равно раунду d
до ближайшего значения Int64
. Но ваше значение d
больше, чем наибольшее возможное значение, которое может быть сохранено в Int64
и, следовательно, ловушках единиц с плавающей запятой.