Недопустимая операция вызова Trunc с плавающей запятой()

я получаю (повторяемое) исключение с плавающей запятой, когда я пытаюсь Trunc() a Real значение.

например:

Trunc(1470724508.0318);

на самом деле фактический код сложнее:

 ns: Real;
 v: Int64;

 ns := ((HighPerformanceTickCount*1.0)/g_HighResolutionTimerFrequency) * 1000000000;
 v := Trunc(ns);

но в конце концов это все равно сводится к:

Trunc(ARealValue);

теперь я не могу повторить это нигде - только в этом месте. Где он терпит неудачу каждый раз.

это не вуду

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

вход в окно CPU

v: = Trunc (ns)

fld qword ptr [ebp-]

это загружает 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

Далее идет кишки функции Trunc Delphi RTL:

@TRUNC:

sub esp,c
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+]  //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 = F32;

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 F32
        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+]

в момент этого звонка ST0 register будет содержать то же значение с плавающей запятой:

enter image description here

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

до этого:

  • sub esp,c: я смотрю, как он толкает стек вниз на 12 байт
  • fstcw word ptr [esp]: я смотрю, как он нажимает $027F в текущий указатель стека
  • fldcw word ptr [cwChop]: я наблюдаю за изменением флагов управления с плавающей запятой
  • fistp qword ptr [esp+]: и он собирается написать Int64 в комнату он сделал на стеке

и затем он падает.

что на самом деле здесь происходит?

это происходит и с другими значениями, это не похоже на то, что что-то не так с этим конкретным значением с плавающей запятой. Но я даже попытался настроить тест в другом месте.

зная, что 8-байтовое шестнадцатеричное значение float:D5EA5E6702098C, я попытался придумать настройку:

var
    ns: Real;
    nsOverlay: Int64 absolute ns;
    v: Int64;
begin
   nsOverlay := d62866a2f270dc;
   v := Trunc(ns);
end;

что дает:

nsOverlay: = $ 41d62866a2f270dc;

mov [ebp-],$a2f270dc
mov [ebp-],d62866

v: = Trunc (ns)

fld qword ptr [ebp-]
call @TRUNC

и по call до @trunc, регистр с плавающей запятой ST0 содержит значение:

enter image description here

но вызов не провал. Он не только каждый раз в этом разделе моего кода.

что может быть возможно, это приводит к тому, что CPU бросает invalid floating point exception?

каково значение cwChop перед загрузкой управляющего слова?

значение cwChop выглядит правильным перед загрузить управляющее слово, F32. Но после нагрузки фактический контрольное слово неверно:

enter image description here

Бонус Треп

фактическая функция, которая терпит неудачу, - это что-то конвертировать высокопроизводительные отсчеты тика в наносекунды:

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 := d62866a2f270dc;
    v := Trunc(ns);

но он отлично работает. Есть что-то о том, когда он вызывается во время модульного теста DUnit.

флаги контрольного слова с плавающей запятой

стандартное управляющее слово Delphi: 32:

32 = 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: 7F

7F = 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 управляющее слово: F32

F32 = 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 флаги после загрузки F32: F72

F72 = 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, мы хотели бы RaiseLastFPError. Лучшее, что я могу придумать-это:

procedure RaiseLastFPError();
var
    statWord: Word;
const
    ERROR_InvalidOperation = ;
//  ERROR_Denormalized = ;
    ERROR_ZeroDivide = ;
    ERROR_Overflow = ;
//  ERROR_Underflow = ;
//  ERROR_InexactResult = ;
begin
    {
        Excellent reference of all the floating point instructions.
        (Intel's 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 = F)
            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 D) <> 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 := 1d6db44ff62b68; //1.35715152325557E020
    d := Round(d); //<--floating point exception
    Self.Caption := FloatToStr(d);
end;

видно, что ST0 register содержит допустимое значение с плавающей запятой. Управляющее слово с плавающей запятой -72. Там флаг исключения с плавающей запятой все ясно:

enter image description here

и затем, как только он выполняется, это недопустимая операция:

enter image description here

  • IE (недопустимая операция) установлен флаг
  • ES (Исключение) установлен флаг

у меня было искушение задать этот вопрос как другой, но это был бы тот же самый вопрос-за исключением этого вызова Round().

2 ответов


проблема возникает в другом месте. Когда ваш код вводит Trunc управляющее слово имеет значение 7F который, IIRC, контрольное слово Windows по умолчанию. Все исключения замаскированы. Это проблема, потому что RTL Delphi ожидает, что исключения будут разоблачены.

и посмотрите на окно FPU, конечно, есть ошибки. Установлены флаги IE и PE. Это IE, который считается. Это означает, что ранее в кодовой последовательности был маскированный недопустимый операция.

тогда вы звоните Trunc который изменяет управляющее слово, чтобы разоблачить исключения. Посмотрите на второй скриншот окна FPU. IE-1, но IM-0. Итак, бум, более раннее исключение поднято, и вы вынуждены думать, что это была ошибка Trunc. Но это было не так.

вам нужно будет отследить резервную копию стека вызовов, чтобы узнать, почему управляющее слово не то, что должно быть в программе Delphi. Это должно быть 32. Скорее всего, вы звоните в некоторые сторонняя библиотека, которая изменяет управляющее Слово и не восстанавливает его. Вам нужно будет найти виновника и взять на себя ответственность, когда любые вызовы этой функции возвращаются.

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

обратите внимание, что нет ничего, чтобы волноваться о несоответствии между 72 и 32 или F72 и F32. Это просто странность с CTRL контрольное слово, что некоторые из байтов зарезервированы и игнорируют ваши увещевания, чтобы очистить их.


ваше последнее обновление по существу задает другой вопрос. Он спрашивает об исключении, вызванном этим кодом:

procedure foo;
var
  d: Real;
  i: Int64;
begin
  d := 1.35715152325557E020;
  i := Round(d);
end;

этот код терпит неудачу, потому что задание Round(), чтобы d до ближайшего Int64 значение. Но ваша ценность d больше, чем максимально возможное значение, которое может быть сохранено в Int64 и, следовательно, ловушки с плавающей запятой.