Jump to content
Jim McKeeth

Delphi (Win32) quiets signaling NaN on function return

Recommended Posts

I found myself down the rabbit hole of IEEE 754 standard on Floating-Point numbers (which Delphi follows), specifically the different values of NaN.... There are multiple potential internal values for NaN, all which evaluate as fsNaN. I found what I believe to be a bug, but I'm curious if anyone else has any thoughts on it. (As I was writing this up I discovered the behavior is only on Win32, so I've decided it must be a bug (RSS-1831), but sharing here anyway because I find the details interesting.)

 

In short: IEEE 754 distinguishes between Signaling NaN and Quiet NaN. Delphi defaults to Quiet NaN, but you can make it signaling manually. Unfortunately, when a float is returned from a function in the Win32 compiler, the quiet flag is set. I was testing to see if Delphi converted it into an exception, which would be understandable, but instead it just silently sets the quiet flag, suppressing the signaling state.

 

Testing in FPC, Delphi Win64, or Delphi Linux64, and the flag value doesn't change, which is what I would expect.

 

Detailed explanation and background

 

IEEE 754 divides a float into 3 parts

 

  1. Sign
  2. Exponent
  3. Fraction

 

For a NaN the Exponent bits are all set ($FF in Single), any value for sign (Delphi's default NaN has it set, making it a negative NaN, which doesn't carry any specific significance), and the Fraction is anything but all 0. This allows for quiet a few different values that all are treated as NaN. What may distinguish the different values of NaN is signaling vs. quiet NaN. The Quiet flag is usually the first bit of the fraction. When the quiet flag isn't set, then it is considered a signaling NaN.

 

The internal the internal representation of a NaN is typically displayed as follows (notice this is reversed from how Delphi stores the value in memory to put the most significant bit first.)

S   Exponent   Fraction
1 | 11111111 | 10000000000000000000000
               ^ The quiet flag  

So a signaling NaN is has an value for Faction without that first bit set.

 

 

What I found in Delphi Win32 is it handles all these values correctly, except that if the quiet flag is missing (making it a signaling NaN), then when the value is returned from a function the quiet flag is set.

 

Before returning from function: (Notice that the debugger recognizes it as negative NaN. Very nice!)

before function return 

This is 1 | 11111111 | 00000000000000000000001 in binary, which doesn't have the Quiet flag set

 

image.thumb.png.4f223e0737eb6cb45188a2a084daa623.png

1 | 11111111 | 10000000000000000000001
After return it isn't the Default NaN, but it is the previous value with the Quiet flag set.

 

Here is some sample code that demonstrates the behavior

// The Delphi Win32 (tested in Delphi 12.1 and 12.2) sets NaN's quiet flag when are returning from a function
// More information on IEEE 754 NaN https://en.wikipedia.org/wiki/NaN#Quiet_NaN
// More information on this code: https://gist.github.com/jimmckeeth/2b4f017917afbae88ee7a3deb75b4ef7
program NaNSignalBug;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;//, SingleUtils in 'SingleUtils.pas';

function is_Signaling(const NaN: Single): Boolean;
begin
  Result := NaN.IsNan and (NaN.Frac and $400000 = 0);
end;

function NaNDetails(const Value: Single): string;
begin
  if value.IsNan then
  begin
    if is_Signaling(value) then
      Result := 'NaN is Signaling'
    else
      Result := 'NaN is Quiet';
  end else
    Result := 'Not a NaN';
end;

procedure MakeSignaling(var NaN: Single);
begin
  NaN.Exp := 255;
  NaN.Frac := NaN.Frac and not (1 shl 22);
  Assert(is_Signaling(NaN));
  Writeln('Manipulated: ',NaNDetails(NaN),#9, '// Line 33');
end;

// When a NaN is returned from a function the Signal bit is set
function SignalingNaN: Single;
begin
  Result := Single.NaN;
  Result.Frac := 1;
  MakeSignaling(Result);
  Assert(is_Signaling(Result));
  Writeln(#9,'SignalingNaN Returning',#9'// Line 43');
end;

function NestedNaN: Single;
begin
  var NaN : Single := SignalingNaN;
  // The quiet bit was set
  Writeln('Returned:    ',NaNDetails(NaN),#9, '// Line 50');
  // without returning it from a function it works fine
  MakeSignaling(NaN);
  Writeln('Manipulated: ',NaNDetails(NaN),#9, '// Line 53');
  Assert(is_Signaling(NaN));
  Writeln(#9,'NestedNaN Returning ',#9,'// Line 55');
  Exit(NaN);
end;


begin
  try
    Writeln(TOSVersion.ToString);
    Writeln('Used: ',SizeOf(NativeInt)*8,'-bit compiler');
    var NaN : Single := NestedNaN;
    // The quiet bit was set
    Writeln('Returned:    ', NaNDetails(NaN),#9,'// Line 66');
    //Assert(is_Signaling(NaN)); // Fails on Win32
    // without returning it from a function it works fine
    MakeSignaling(NaN);
    Writeln('Manipulated: ', NaNDetails(NaN),#9,'// Line 70');
    Assert(is_Signaling(NaN));
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  readln;
end.

and here is the output

Windows 11 (Version 23H2, OS Build 22631.4169, 64-bit Edition)
Used: 32-bit compiler
Manipulated: NaN is Signaling   // Line 33
        SignalingNaN Returning  // Line 43
Returned:    NaN is Quiet       // Line 50
Manipulated: NaN is Signaling   // Line 33
Manipulated: NaN is Signaling   // Line 53
        NestedNaN Returning     // Line 55
Returned:    NaN is Quiet       // Line 66
Manipulated: NaN is Signaling   // Line 33
Manipulated: NaN is Signaling   // Line 70

and a more detailed look at the NaN values

Default NaN
Value of :                     Nan
Special  : fsNaN
Sign     : TRUE
Exponent : 255
Fraction : 4194304
Size     : 4
InvertHex: $FFC00000
1 | 11111111 | 10000000000000000000000
-----------------

Singnaling NaN
Value of :                     Nan
Special  : fsNaN
Sign     : TRUE
Exponent : 255
Fraction : 1
Size     : 4
InvertHex: $FF800001
1 | 11111111 | 00000000000000000000001
-----------------

Returned from Function
Value of :                     Nan
Special  : fsNaN
Sign     : TRUE
Exponent : 255
Fraction : 4194305
Size     : 4
InvertHex: $FFC00001
1 | 11111111 | 10000000000000000000001

@David Heffernan it was suggested I tag you on this...

NaNSignalBug.dpr

  • Like 2

Share this post


Link to post

I don't have anything later than 11.3 installed. So there are likely changes that I'm not familiar with. 

 

The one thing that jumps to my mind is the ABI. Floating point return values travel via ST(0) in x86 but by a general purpose register in x64 (can't remember which). 

 

This means that it has traditionally not been possible to return SNaN by function return value in 32 bit, although it is possible to in 64 bit. That's because loading into ST(0) triggers invalid op. 

 

But now fp exceptions are masked by default. 

 

Anyway, that's all I know right now, not at a computer. 

  • Like 1

Share this post


Link to post

The result value is coming from FPU register ST(0) like @David Heffernan told.

The issue is that the code load in ST(0) the correct signaling NAN, but become quieted NAN in ST(0).

 

This is not an Issue from Embarcadero, but may be an hardware setting. I am far from FPU registers (working with them at time of  I486DX :classic_blink:).

 

image.thumb.png.aec1d40d60eafa7336f68607aa2ea7e4.png

 

EDIT: From what I read in the Intel manuals, it seems that the impersonation is internally QNAN if the FPU "invalid operation" exception is masked. (ref 4.8.3.6 of Intel manual "325462-sdm-vol-1-2abcd-3abcd-4"). There are many tables inside that show how hardware treats the NAN.

Edited by DelphiUdIT
  • Like 1

Share this post


Link to post

https://developercommunity.visualstudio.com/t/signaling-nan-float-double-becomes-quiet-nan-when/903305#T-N1065496

https://stackoverflow.com/questions/22816095/signalling-nan-was-corrupted-when-returning-from-x86-function-flds-fstps-of-x87

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57484

Quote

On an x86 target using the legacy x87 instructions and the 80-bit registers, a load of a 64-bit or 32-bit value in memory into the 80-bit registers counts as a format conversion and an signaling NaN input will turn into a quiet NaN in the register format.

So apparently this is just the way things are on x86.

  • Thanks 1

Share this post


Link to post

To summarize:

 

x86 (32-bit):

1) "Quiet NAN" can always be used;

2) UnMasked Invalid Operation :
- "Signaling NAN" throws an exception if used with FPU registers (i.e function return value);

3) Masked Invalid Operation:
- "Signaling NAN" is switched to "Quiet NAN" if used with FPU registers (i.e function return value);

N.B.: if you pass the "Signaling NAN" via function parameter then you can use it 'cause no FPU register is involving.

 

 

x64 (64-bit):

1) "Quiet NAN" can always be used;

2) "Signaling NAN" can always be used;

Edited by DelphiUdIT
  • Thanks 1

Share this post


Link to post

Ultimately, if exceptions are masked then there's no behavioural difference between SNaN and QNaN. 

 

Seems an odd design choice for x86 to change the NaN type though. Presumably there's a reason I'm not aware of. 

 

Normally I think I know my way around floating point, but when exceptions are masked is the great unknown for me. I've never been there. And when I explored moving to 12 I just unmasked them!!! 

Edited by David Heffernan
  • Like 1

Share this post


Link to post
25 minutes ago, David Heffernan said:

Normally I think I know my way around floating point, but when exceptions are masked is the great unknown for me. I've never been there. And when I explored moving to 12 I just unmasked them!!! 

Uhmm... I don't know for the past, but in Delphi12 NaN is defined like 0.0 / 0.0 (yes zero divide 0), so don't try to use it with unmasked exception ...

 

image.thumb.png.b6f596cf01643dbcf689cbefe83f5be8.png

Share this post


Link to post
7 hours ago, DelphiUdIT said:

Uhmm... I don't know for the past, but in Delphi12 NaN is defined like 0.0 / 0.0 (yes zero divide 0), so don't try to use it with unmasked exception ...

That's evaluated to a quietnan at compile time, just as it has always been

Share this post


Link to post
1 hour ago, David Heffernan said:

That's evaluated to a quietnan at compile time, just as it has always been

You are right, my brain went on Holiday ... 😞

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×