Я получаю (повторяемое) исключение с плавающей запятой, когда я пытаюсь 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)
Вызов успешно завершен, а регистр с плавающей запятой содержит соответствующее значение:
Далее - это фактический вызов функции 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 будет содержать одно и то же значение с плавающей запятой:
Примечание: внимательный наблюдатель заметит, что значение в приведенном выше скриншоте не соответствует первому снимку экрана. Это потому, что я взял его в другой перспективе. Я бы предпочел не переустанавливать все константы в вопросе, чтобы сделать их согласованными, но поверьте мне: это то же самое, когда я достигаю инструкции
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 с плавающей запятой содержит значение:
Однако вызов не завершается с ошибкой. Он только терпит неудачу, каждый раз в этом одном разделе моего кода.
Что может произойти, что заставляет процессор бросать invalid floating point exception
?
Каково значение cwChop
перед загрузкой управляющего слова?
Значение cwChop
выглядит корректно перед словом управления нагрузкой $1F32
. Но после загрузки управляющее слово фактическое неверно:
Бонус 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
. Флаг исключения с плавающей запятой все ясен:
И затем, как только он будет выполнен, это приведет к недействительной операции:
-
IE
(Неверная операция) установлен флаг Установлен флаг -
ES
(Исключение)
У меня возникло соблазн задать этот вопрос как другой вопрос, но это был бы тот же самый вопрос, за исключением того, что на этот раз вызывал Round()
.