Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi and MSVC do not compare +NAN with zero the same way

Tags:

c

ieee-754

delphi

I am porting C code to Delphi and find an issue in the way the compilers (Delphi 10.4.1 and MSVC2019, both targeting x32 platform) handle comparison of +NAN to zero. Both compilers use IEEE754 representation for double floating point values. I found the issue because the C-Code I port to Delphi is delivered with a bunch of data to validate the code correctness.

The original source code is complex but I was able to produce a minimal reproducible application in both Delphi and C.

C-Code:

#include <stdio.h>
#include <math.h>

double AngRound(double x) {
    const double z = 1 / (double)(16);
    volatile double y;
    if (x == 0) 
        return 0;
    y = fabs(x);
    /* The compiler mustn't "simplify" z - (z - y) to y */
    if (y < z)
        y = z - (z - y);      // <= This line is *NOT* executed
    if (x < 0)
        return -y;
    else
        return y;             // <= This line is executed
}

union {
    double d;
    int bits[2];
} u;


int main()
{
    double lon12;
    double ar;
    int    lonsign;

    // Create a +NAN number IEEE754
    u.bits[0] = 0;
    u.bits[1] = 0x7ff80000;

    lon12    = u.d;                // Debugger shows lon12 is +nan
    if (lon12 >= 0)
        lonsign = 1;
    else
        lonsign = -1;              // <= This line is executed
    // Now lonsign is -1

    ar = AngRound(lon12);
    // Now ar is +nan

    lon12 = lonsign * ar;
    // Now lon12 is +nan
}

Delphi code:

program NotANumberTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
    TRec = record
       case t : Boolean of
       TRUE:  (d    : Double);
       FALSE: (bits : array [0..1] of Cardinal);
    end;

function AngRound(x : Double) : Double;
const
    z : Double = 1 / Double(16);
var
    y : Double;
begin
    if x = 0 then
        Result := 0
    else begin
        y := abs(x);
        if y < z then
            // The compiler mustn't "simplify" z - (z - y) to y
            y := z - (z - y);           // <= This line is executed
        if x < 0 then
            Result := -y                // <= This line is executed
        else
            Result := y;
    end;
end;

var
    u       : TRec;
    lon12   : Double;
    lonsign : Integer;
    ar      : Double;
begin
    // Create a +NAN number IEEE754
    u.bits[0] := 0;
    u.bits[1] := $7ff80000;

    lon12 := u.d;                       // Debugger shows lon12 is +NAN
    if lon12 >= 0 then
        lonsign := 1                    // <= This line is executed
    else
        lonsign := -1;
    // Now lonsign is +1

    ar := AngRound(lon12);
    // Now ar is -NAN

    lon12 := lonsign * ar;
    // Now lon12 is -NAN
end.

I have marked the lines with are executed after a comparison. Delphi evaluate (lon12 >= 0) to TRUE when lon12 variable equal +NAN. MSVC evaluate (lon12 >= 0) to FALSE when lon12 variable equal +NAN.

lonsign has different values in C and Delphi.

AngRound receiving +NAN as argument return different values.

Final value of lon12 is (fatally) different.

Machine code generated by the compilers are different:

Delphi generated machine code:

Delphi generated machinecode

MSVC2019 generated machine code:

MSVC2019 generated machine code

The comparison result seems more logical in Delphi: (lon12 >= 0) is TRUE when lon12 is +NAN. Does this means the bug is in MSVC2019 compiler? Should I consider the test data set of the original C-Code carry in error?

like image 822
fpiette Avatar asked Feb 01 '21 10:02

fpiette


People also ask

Can you compare NaN?

Unlike all other possible values in JavaScript, it is not possible to use the equality operators (== and ===) to compare a value against NaN to determine whether the value is NaN or not, because both NaN == NaN and NaN === NaN evaluate to false . The isNaN() function provides a convenient equality check against NaN .

What is double NaN in C#?

Overview. In C#, the Double. NaN field represents a value that is not a number. It is constant.

How to replace NaN values with zeros in a Dataframe?

You can accomplish the same task, of replacing the NaN values with zeros, by using NumPy: For our example, you can use the following code to perform the replacement: As before, the two NaN values became 0’s: For the first two cases, you only had a single column in the dataset. But what if your DataFrame contains multiple columns?

What does Nan mean in Visual Basic?

Check this out: To remain mathematically correct, VB.NET gives you the answer NaN (Not a Number) for some calculations such as 0 / 0. VB.NET can also tell the difference between positive infinity and negative infinity: "negative infinity.")

What is the smallest double value greater than zero?

In addition to PositiveInfinity and NegativeInfinity, VB.NET also provides Epsilon, the smallest positive Double value greater than zero. Keep in mind that all of these new capabilities of VB.NET are only available with floating point (Double or Single) data types.

What is NaN (Not a number)?

To remain mathematically correct, VB.NET gives you the answer NaN (Not a Number) for some calculations such as 0 / 0. VB.NET can also tell the difference between positive infinity and negative infinity: "negative infinity.")


1 Answers

First of all, your Delphi program does not behave as you describe, at least on the Delphi version readily available to me, XE7. When your program is run, an invalid operation floating point exception is raised. I'm going to assume that you have actually masked floating point exceptions.

Update: It turns out that at some time between XE7 and 10.3, Delphi 32 bit codegen switched from fcom to fucom which explains why XE7 sets the IA floating point exception, but 10.3 does not.

Your Delphi code is very far from minimal. Let's try to make a truly minimal example. And let's look at other comparison operators.

{$APPTYPE CONSOLE}

uses
  System.Math;

var
  d: Double;
begin
  SetFPUExceptionMask(exAllArithmeticExceptions);
  SetSSEExceptionMask(exAllArithmeticExceptions);
  d := NaN;
  Writeln(d > 0);
  Writeln(d >= 0);
  Writeln(d < 0);
  Writeln(d <= 0);
  Writeln(d = d);
  Writeln(d <> d);
end.

Under 32 bit in XE7, this outputs

TRUE
TRUE
FALSE
FALSE
TRUE
FALSE

Under 32 bit in 10.3.3 (and 10.4.1 as you report in a comment below), this outputs

TRUE
TRUE
TRUE
TRUE
FALSE
TRUE

Under 64 bit in XE7 and 10.3.3 (and 10.4.1 as your report), this outputs

FALSE
FALSE
FALSE
FALSE
FALSE
TRUE

The 64 bit output is correct. The 32 bit output for both variants are incorrect. This we we can see by referring to What is the rationale for all comparisons returning false for IEEE754 NaN values?

all comparisons with the operators ==, <=, >=, <, > where one or both values is NaN returns false, contrary to the behaviour of all other values.

For your 32 bit Delphi code, you will need to workaround this bug and include special case code whenever it needs to handle such comparisons. Unless of course, by some happy chance, that you are using 10.4 and it already fixes the issue.

like image 73
David Heffernan Avatar answered Oct 20 '22 09:10

David Heffernan