How can I make signaling NaNs easy to work with?

牧云@^-^@ 提交于 2020-01-02 01:18:29

问题


The IEEE754 standard defines two classes of NaN, the quiet NaN, QNaN, and the signaling NaN, SNaN. When an SNaN is loaded into a floating point register, an exception is raised by the floating point unit.

QNaN is available to Delphi code through the constant named NaN that is declared in Math. The definition of that constant is:

const
  NaN = 0.0 / 0.0;

I would like to be able to use something similar to declare a constant that is a signaling NaN, but have not yet found a way to do that.

Naively you might write this code:

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

But the ABI for floating point return values means that the SNaN is loaded into a floating point register so that it can be returned. Naturally that leads to an exception which rather defeats the purpose.

So you are then led to writing code like this:

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

Now, this works, but it's very inconvenient. Suppose you need to pass an SNaN to another function. Ideally you would like to write:

Foo(SNaN)

but instead you have to do this:

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

So, after the build-up, here's the question.

Is there any way to write x := SNaN and have the floating point variable x assigned a value that is a signaling NaN?


回答1:


This declaration solves it at compile time:

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

The compiler still treats the SNaN as a constant.

Trying to assign a value to SNaN will give a compile time error: 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"

Should you have the compiler directive $WRITEABLECONSTS ON (or $J+), this could be turned off temporarily to ensure not altering SNaN.

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

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

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



回答2:


Here's another workaround:

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

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

The debugger shows IEEE754.SNaN as +NAN, however when you access it you'll still get a floating point exception. A workaround for that could be:

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;



回答3:


You can inline the function:

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

But it will depend on the optimization and compiler mood.

I've seen some functions not inlined, without any clear understanding from the context. I do not like either relying on inlining.

What I would better do, and which will work on all versions of Delphi, is to use a global variable:

var
  SNaN: double;

Then set it in the initialization block of the unit:

const
  SNaN64 = $7FF7FFFFFFFFFFFF;

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

Then you will be able to use SNaN just as a regular constant. That is, you can write code as expected:

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

In the IDE debugger, it will be shown as "test = +NAN", which is the expected result, I suppose.

Note that using this SNaN will raise an exception when it is read into the FPU stack (e.g. if test=0 then) so you have to check the value at binary level... this is the reason why I defined a SNaN64 constant, which will make very fast code by the way.

  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);

You can change this behavior by changing the x87 exception register:

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

I suppose this to be set globally for your program, in all extend of the code which will have to handle this SNaN constant.




回答4:


I use a function:

Function UndefinedFloat : double
Begin
  Result := Nan
End;

This then works 
Var 
  MyFloat : double;

Begin
  MyFloat := UndefinedFloat;



回答5:


Here's a rather dirty way to do it, that results in very clean code for the consumer.

unit uSNaN;

interface

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

implementation

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

end.

I was expecting the linker to put SNaN in a read-only segment of the executable but it appears not to do so. In any case, even if it did you could use VirtualProtect to get around that for the duration of the assignment.



来源:https://stackoverflow.com/questions/16249748/how-can-i-make-signaling-nans-easy-to-work-with

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!