调用Trunc()的浮点运算无效

Ian*_*oyd 16 delphi delphi-5

当我尝试Trunc()一个Real值时,我得到一个(可重复的)浮点异常.

例如:

Trunc(1470724508.0318);
Run Code Online (Sandbox Code Playgroud)

实际上,实际代码更复杂:

 ns: Real;
 v: Int64;

 ns := ((HighPerformanceTickCount*1.0)/g_HighResolutionTimerFrequency) * 1000000000;
 v := Trunc(ns);
Run Code Online (Sandbox Code Playgroud)

但最终它仍归结为:

Trunc(ARealValue);
Run Code Online (Sandbox Code Playgroud)

现在,我不能在其他任何地方重复它 - 就在这一点.每次失败的地方.

这不是伏都教

幸运的是计算机并不神奇.英特尔CPU执行非常具体的可观察操作.所以我应该能够找出浮点运算失败的原因.

进入CPU窗口

v:= Trunc(ns)

fld qword ptr [ebp-$10]
Run Code Online (Sandbox Code Playgroud)

这会将ebp- $ 10处的8字节浮点值加载到浮点寄存器中ST0.

内存地址[ebp- $ 10]的字节数为:

0018E9D0: 6702098C 41D5EA5E    (as DWords)
0018E9D0: 41D5EA5E6702098C     (as QWords)
0018E9D0:   1470724508.0318    (as Doubles)
Run Code Online (Sandbox Code Playgroud)

调用成功,浮点寄存器包含适当的值:

在此输入图像描述

接下来是对RTL Trunc函数的实际调用:

call @TRUNC
Run Code Online (Sandbox Code Playgroud)

接下来是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
Run Code Online (Sandbox Code Playgroud)

或者我想我可以从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;
Run Code Online (Sandbox Code Playgroud)

在实际的help操作期间发生异常.

fistp qword ptr [esp+$04]
Run Code Online (Sandbox Code Playgroud)

在此调用时,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;
Run Code Online (Sandbox Code Playgroud)

这使:

nsOverlay:= $ 41d62866a2f270dc;

mov [ebp-$08],$a2f270dc
mov [ebp-$04],$41d62866
Run Code Online (Sandbox Code Playgroud)

v:= Trunc(ns)

fld qword ptr [ebp-$08]
call @TRUNC
Run Code Online (Sandbox Code Playgroud)

callto 点@trunc,浮点寄存器ST0包含一个值:

在此输入图像描述

但呼叫也没有失败.它只会失败,每次都在我的代码的这一部分.

可能发生什么导致CPU抛出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;
Run Code Online (Sandbox Code Playgroud)

我创建了所有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);
Run Code Online (Sandbox Code Playgroud)

但它运作正常.有关在DUnit单元测试期间调用它的事情.

浮点控制字标志

德尔福的标准控制字$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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

所有CPU正在执行的操作是打开保留的未使用的掩码位.

RaiseLastFloatingPointError()

如果你要为Windows开发程序,你真的需要接受浮点异常应该被CPU屏蔽的事实,这意味着你必须自己监视它们.喜欢Win32Check或者RaiseLastWin32Error,我们喜欢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'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 = $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;
Run Code Online (Sandbox Code Playgroud)

一个可重复的案例!

我发现一个案例,当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;
Run Code Online (Sandbox Code Playgroud)

您可以看到ST0寄存器包含有效的浮点值.浮点控制字是$1372.有浮点异常标志都清楚:

在此输入图像描述

然后,一旦执行,它就是一个无效的操作:

在此输入图像描述

  • IE (无效操作)标志已设置
  • ES 设置(例外)标志

我很想把这个问题作为另一个问题,但这是完全相同的问题 - 除了这次打电话Round().

Dav*_*nan 11

问题发生在其他地方.当您的代码输入时Trunc,控制字被设置为$027FIIRC,即默认的Windows控制字.这有掩盖的所有例外.这是一个问题,因为Delphi的RTL期望异常被揭露.

看看FPU窗口,确定有错误.IE和PE标志都已设置.重要的是IE.这意味着在代码序列的早期有一个被屏蔽的无效操作.

然后调用Trunc哪个修改控制字以取消屏蔽异常.看看你的第二个FPU窗口截图.IE是1,但IM是0.所以繁荣,早先的例外被提出,你被认为是它的错Trunc.它不是.

您需要跟踪调用堆栈以找出控制字不是它应该在Delphi程序中的原因.它应该是$1332.很可能你正在调用一些第三方库来修改控制字并且不会恢复它.每当对该函数的任何调用返回时,您都必须找到罪魁祸首并负责.

一旦你将控制字恢复到控制之下,你就会发现这个异常的真正原因.显然存在非法的FP操作.一旦控制字取消屏蔽异常,就会在正确的位置引发错误.

请注意,没有什么可担心$1372和之间的差异$1332,或者$1F72$1F32.这只是一个奇怪的CTRL控制字,一些字节是保留的,并忽略你的劝告,以清除它们.

  • 在这方面,这种控制很糟糕.Delphi的RTL没有帮助.你知道Set8087CW不是线程安全的吗?我有一个关于浮点问题的严肃QC报告,最终提出了重新设计和修复运行时的详细建议.我的程序修复了RTL.虽然不能看到Emba做过这件事. (4认同)

Dav*_*nan 10

您的最新更新基本上会提出一个不同的问题.它询问此代码引发的异常:

procedure foo;
var
  d: Real;
  i: Int64;
begin
  d := 1.35715152325557E020;
  i := Round(d);
end;
Run Code Online (Sandbox Code Playgroud)

此代码失败,因为作业Round()是舍d入到最接近的Int64值.但是您的值d大于可以存储在Int64浮点单元陷阱中的最大可能值.

  • 我在运行代码时未发现异常。听起来您想采用其他方法。这意味着我的代码无济于事。我确实比RTL中的`_TRUNC`好,但是它假定了未屏蔽的异常。这是比RTL中更好的实现。我的策略是继续处理未公开的异常。 (2认同)