【问题标题】:How can I make signaling NaNs easy to work with?如何使信号 NaN 易于使用?
【发布时间】:2013-04-28 04:08:50
【问题描述】:

IEEE754 标准定义了两类 NaN,静默 NaN,QNaN,和信令 NaN,SNaN。当 SNaN 加载到浮点寄存器时,浮点单元会引发异常。

QNaN 可通过在 Math 中声明的名为 NaN 的常量用于 Delphi 代码。该常量的定义是:

const
  NaN = 0.0 / 0.0;

我希望能够使用类似的东西来声明一个常量,它是一个信号 NaN,但还没有找到一种方法来做到这一点。

您可能会天真地编写以下代码:

function SNaN: Double;
begin
  PInt64(@Result)^ := $7FF7FFFFFFFFFFFF;//this bit pattern specifies an SNaN
end;

但是浮点返回值的 ABI 意味着 SNaN 被加载到浮点寄存器中,以便可以返回。这自然会导致一个与目的相反的异常。

然后你会被引导编写这样的代码:

procedure SetToSNaN(out D: Double);
begin
  PInt64(@D)^ := $7FF7FFFFFFFFFFFF;
end;

现在,这可行,但非常不方便。假设您需要将 SNaN 传递给另一个函数。理想情况下你想写:

Foo(SNaN)

但你必须这样做:

var
  SNaN: Double;
....
SetToSNaN(SNaN);
Foo(SNaN);

所以,在构建之后,问题来了。

有没有办法写x := SNaN 并让浮点变量x 分配一个值,它是一个信号NaN?

【问题讨论】:

  • 您是否尝试过内联您的第一种方法?
  • @UweRaabe 事实上我有。我想让其他人写下这个答案,以便他们获得代表。我仍然对依赖内联感到有些不安。如果函数调用以某种方式没有内联,那么就繁荣了。

标签: delphi


【解决方案1】:

这个声明在编译时解决它:

const
  iNaN : UInt64 = $7FF7FFFFFFFFFFFF;
var
  SNaN : Double absolute iNaN;

编译器仍将SNaN 视为常量。

尝试为SNaN 赋值会产生编译时错误:E2064 Left side cannot be assigned to

procedure DoSomething( var d : Double);
begin
  d := 2.0;
end;

SNaN := 2.0; // <-- E2064 Left side cannot be assigned to
DoSomething( SNaN); // <--E2197 Constant object cannot be passed as var parameter
WriteLn(Math.IsNaN(SNaN)); // <-- Writes "true"

如果您有编译器指令$WRITEABLECONSTS ON(或$J+),可以暂时关闭它以确保不会更改SNaN

{$IFOPT J+}
   {$DEFINE UNDEFWRITEABLECONSTANTS}
   {$J-}
{$ENDIF}

const
  iNaN : UInt64 = $7FF7FFFFFFFFFFFF;
var
  SNaN : Double ABSOLUTE iNaN;

{$IFDEF UNDEFWRITEABLECONSTANTS}
   {$J+}
{$ENDIF}

【讨论】:

  • +1 这可以解决问题,但我会对生活在变量而不是常量中的值感到有点不安。
  • 给SNaN赋值会报编译错误:E2064 Left side cannot be assigned to
【解决方案2】:

这是另一种解决方法:

type
  TFakeRecord = record
    case Byte of
      0: (SNaN: Double);
      1: (i: Int64);
  end;

const
  IEEE754: TFakeRecord = ( i: $7FF7FFFFFFFFFFFF);

调试器将 IEEE754.SNaN 显示为 +NAN,但是当您访问它时,您仍然会得到一个浮点异常。解决方法可能是:

type
  ISet8087CW = interface
  end;

  TISet8087CW = class(TInterfacedObject, ISet8087CW)
  protected
    OldCW: Word;
  public
    constructor Create(const NewCW: Word);
    destructor Destroy; override;
  end;

  TIEEE754 = record
    case Byte of
      0: (SNaN: Double);
      1: (i: Int64);
  end;

const
  IEEE754: TIEEE754 = ( i: $7FF7FFFFFFFFFFFF);

{ TISet8087CW }

constructor TISet8087CW.Create(const NewCW: Word);
begin
  OldCW := Get8087CW;
  Set8087CW(NewCW);
  inherited Create;
end;

destructor TISet8087CW.Destroy;
begin
  Set8087CW(OldCW);
  inherited;
end;

procedure TForm6.Button4Click(Sender: TObject);
var
  CW: ISet8087CW;
begin
  CW := TISet8087CW.Create($133F);
  Memo1.Lines.Add(Format('SNaN: %f', [IEEE754.SNaN]));
end;

【讨论】:

  • 看起来 RAII 模式正在德尔福领域起飞。 ;-)
  • +1 我非常喜欢变体记录。顺便问一下,你知道Set8087CW 不是线程安全的吗?
  • Delphi 中接口控制生命周期的问题通常是最小的范围单位是过程。这太粗糙了。它在 C++ 中运行良好,因为块引入了新的作用域。
  • @Remko 我不认为 x86 上的任何 RTL 函数调用 Set8087CW。但是在 x64 上的一些实现可以(嗯,x64 上的 SetMXSCR 与 Set8087CW 具有相同的缺陷)。这意味着在一个线程中调用标准 RTL 函数可以改变另一个线程中的 fp 单元状态。这真是令人震惊。我在这里写过这个:qc.embarcadero.com/wc/qcmain.aspx?d=107411
  • @DavidHeffernan,我尝试在您的 QC 报告中下载附件,但该文件存储为 .htm 文件。将文件重命名为 .zip 解决了该问题。顺便说一句,出色的分析和建议。
【解决方案3】:

你可以内联函数:

function SNaN: Double; inline;
begin
  PInt64(@Result)^ := $7FF7FFFFFFFFFFFF;
end;

但这将取决于优化和编译器的心情。

我见过一些没有内联的函数,从上下文中没有任何清晰的理解。我不喜欢依赖内联。

我最好的做法是使用全局变量,并且适用于所有版本的 Delphi:

var
  SNaN: double;

然后在单元的initialization块中设置:

const
  SNaN64 = $7FF7FFFFFFFFFFFF;

initialization
  PInt64(@SNaN)^ := SNaN64;
end.

然后您将能够将SNaN 用作常规常量。也就是说,你可以按预期编写代码:

var test: double;
...
  test := SNaN;

在 IDE 调试器中,它会显示为“test = +NAN”,我想这是预期的结果。

请注意,使用此SNaN 将在将其读入 FPU 堆栈时引发异常(例如 if test=0 then),因此您必须检查二进制级别的值...这就是我定义 @ 的原因987654329@ 常量,顺便说一下,这将使代码变得非常快。

  toto := SNaN;
  if PInt64(@toto)^<>SNaN64 then // will run and work as expected 
    DoubleToString(toto);
  if toto<>SNaN then // will raise an EInvalidOp at runtime
    DoubleToString(toto);

您可以通过更改 x87 异常寄存器来更改此行为:

backup := Set8087CW($133F);
try
  ..
finally
   Set8087CW(backup);
end;

我想这是为你的程序全局设置的,在代码的所有扩展中都必须处理这个SNaN 常量。

【讨论】:

  • 引发异常不是信号 NaN 的全部目的吗?
  • 根据我的经验,如果函数是在首先编译的单元的实现部分的start处定义的,那么该函数将始终内联。
  • 关于 SNaN 的测试,有多个位模式是 SNaN。所以你需要一个更好的测试。 Math 中的 IsNaN 函数是一个很好的起点。
  • @DavidHeffernan 但是我怀疑您主要是想测试 SNaN 值是否已由您自己的代码设置,对吧?在这种情况下,在实践中测试 SNaN64 位模式就足够了。我想回答你的问题,而不是所有即将到来的问题。 :) 但是对您的评论 +1。
  • @UweRaabe 引发异常......但在功能范围内,就问题而言。你不能怪我,因为我明确了异常范围,并给出了解决方法。这就是为什么我在 X87 异常寄存器上添加了一些细节。
【解决方案4】:

我使用一个函数:

Function UndefinedFloat : double
Begin
  Result := Nan
End;

This then works 
Var 
  MyFloat : double;

Begin
  MyFloat := UndefinedFloat;

【讨论】:

  • 这是一个 QNaN 但问题是关于 SNaN
  • 对不起。我不明白其中的区别。
【解决方案5】:

这是一种相当肮脏的方法,它为消费者提供了非常干净的代码。

unit uSNaN;

interface

const
  SNaN: Double=0.0;//SNaN value assigned during initialization

implementation

initialization
  PInt64(@SNaN)^ := $7FF7FFFFFFFFFFFF;

end.

我期待链接器将SNaN 放在可执行文件的只读段中,但它似乎没有这样做。无论如何,即使是这样,您也可以在作业期间使用VirtualProtect 来解决这个问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-15
    • 2017-06-28
    • 1970-01-01
    • 2021-02-02
    • 1970-01-01
    • 2017-12-23
    • 1970-01-01
    相关资源
    最近更新 更多