Неверная операция с плавающей запятой, вызывающая 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 и, следовательно, ловушках единиц с плавающей запятой.