Search code examples
cdelphiieee-754

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


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?


Solution

  • 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.