【问题标题】:Invalid floating point operation calling Trunc()调用 Trunc() 的浮点运算无效
【发布时间】:2014-03-05 21:57:36
【问题描述】:

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

例如:

Trunc(1470724508.0318);

实际上实际代码更复杂:

 ns: Real;
 v: Int64;

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

但最终还是归结为:

Trunc(ARealValue);

现在,我不能在其他任何地方重复它——就在这个地方。每次都失败的地方。

这不是巫毒教

幸运的是,计算机并不是魔法。英特尔 CPU 执行非常具体的可观察操作。所以我应该能够弄清楚为什么浮点运算失败了。

进入 CPU 窗口

v := Trunc(ns)

fld qword ptr [ebp-$10]

这会将 ebp-$10 处的 8 字节浮点值加载到浮点寄存器 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包含一个值:

但调用确实失败。它只会在我的代码的这一部分中每次都失败。

可能发生什么导致 CPU 抛出 invalid floating point exception

cwChop 在加载控制字之前的值是多少?

cwChop 的值在加载控制字之前看起来是正确的,$1F32。但是加载后,实际控制字错了:

奖金聊天

实际失败的功能是将高性能滴答计数转换为纳秒:

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;

我创建了所有中间临时变量来尝试追踪失败的位置。

我什至尝试使用它作为模板来尝试重现它:

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 单元测试期间调用它的时间有所不同。

浮点控制字标志

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)

The Windows API required value:$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 

加载$1F32后的CTRL标志:$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 

CPU 所做的只是打开一个保留的、未使用的掩码位。

RaiseLastFloatingPointError()

如果您要为 Windows 开发程序,您确实需要接受这样一个事实:浮点异常应该被 CPU 屏蔽,这意味着您必须自己注意它们。像Win32CheckRaiseLastWin32Error,我们想要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;

一个可重现的案例!

我发现了一个案例,当 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()

【问题讨论】:

  • 一个可能的罪魁祸首似乎是cwChop 的值。它是否已被导致故障位置的某些东西损坏?
  • @500-InternalServerError 我想我明白你在看什么。它被指示load 更正标志,但之后CTRL 寄存器错误。
  • 你可能想看看这个:wiert.me/2009/05/06/…
  • 顺便问一句好问题。照常。截图有我需要的一切。不用担心 1f32 和 1f72 之间的差异。 CTRL 寄存器总是这样做的。它是寄存器中未使用的保留部分。一切如预期。
  • 将float转换为Int64(Round和Trunc),当float大于maxInt64时会导致浮点异常。

标签: delphi delphi-5


【解决方案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 控制字的一个奇怪之处,其中一些字节被保留并且忽略你清除它们的劝告。

【讨论】:

  • 该死的。 I'm intentionally doing Set8087CW($027F) at program startup because the embedded TWebBrowser that drives the interface。确实需要有人定义一个 Windows 调用约定并坚持下去!
  • @Ian 是的,这个问题多年来让我发疯。这是一个不断给予的问题。我们的代码对第三方库更改控制字有很多防御措施。使用网络浏览器控件,我不知道是否可以在您调用时输入 027F 并稍后恢复。有相当数量的异步不存在。让它变得棘手。
  • 在这方面控制很糟糕。德尔福的 RTL 没有帮助。你知道 Set8087CW 不是线程安全的吗?我有大量关于浮点问题的 QC 报告,最终提出了重新设计和修复运行时的详细建议。我的程序修复了 RTL。不过,我看不到 Emba 会这样做。
  • @Ian - math.pas 中有 ClearExceptions。我检查了 D7,但不知道 D5。虽然它不返回状态,但它是一个过程。但是您似乎已经找到了方法;呼叫特朗克。 :)
  • @Tlama ClearExceptions 实际上并没有引发任何异常。 RaisePending 标志意味着“让任何挂起的操作完成,如果掩码关闭,它将引发异常”。有没有办法让 Intel CPU 触发标准的 SIGTRAP;它在哪里向 Windows 发出信号,谁通过标准异常机制向我发出信号?还是我必须非常笨拙,自己拿到标志,检查它们,然后自己提出EZeroDivideEOverflowEUnderflow等等?理想情况下,CPU 会有 ThrowMaskedErrorsNowPlease 指令。
【解决方案2】:

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

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

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

【讨论】:

  • 是的,当我睡眠不足时会发生这种情况。我需要在那个头上好好敲打一下。
  • 对我来说很容易代表! ;-)
  • 所以,你说你修补了 RTL。您是否曾经创建过SafeRound()SafeTrunc(),在更改舍入模式时不取消屏蔽异常,清除标志,执行操作,然后检查标志,并引发适当的异常(或至少存储旧掩码,在取消掩码时更改舍入模式,执行操作,fwait,捕获异常,将掩码放回原处,然后重新抛出)?我开始写SafeTruncSafeRound,然后理想情况下我会定义让他们接管_TRUNC_ROUND。但如果它已经写好了,对我来说会更容易。
  • 我运行我的代码时未屏蔽异常。听起来你想采取不同的方法。这意味着我的代码无济于事。我确实有一个比 RTL 中更好的_TRUNC,但它假定了未屏蔽的异常。这只是比 RTL 中更好的实现。我的策略是继续处理未屏蔽的异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-23
  • 1970-01-01
相关资源
最近更新 更多