Delphi 和 MSVC 不会以相同的方式将 +NAN 与零进行比较

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

我正在将 C 代码移植到 Delphi 并发现编译器(Delphi 10.4.1 和 MSVC2019,均针对 x32 平台)处理 +NAN 与零的比较的方式存在问题。两个编译器都使用 IEEE754 表示法表示双精度浮点值。我发现这个问题是因为我移植到 Delphi 的 C 代码附带了一堆数据来验证代码的正确性。

原始源代码很复杂,但我能够在 Delphi 和 C 中生成一个最小的可重现应用程序。

C 代码:

#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代码:

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] := ff80000;

    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.

我标记了比较后执行的行。 Delphi 当 lon12 变量等于 +NAN 时,将 (lon12 >= 0) 计算为 TRUE。 当 lon12 变量等于 +NAN.

时,MSVC 将 (lon12 >= 0) 评估为 FALSE

lonsign 在 C 和 Delphi 中有不同的值。

AngRound 接收 +NAN 作为参数return 个不同的值。

lon12 的最终值(致命)不同。

编译器生成的机器码不同:

Delphi 生成的机器码:

MSVC2019生成的机器码:

比较结果在Delphi中似乎更符合逻辑:(lon12 >= 0) is TRUE when lon12 is +NAN.这是否意味着该错误存在于 MSVC2019 编译器中?我应该考虑原始C-Code的测试数据集进位错误吗?

首先,您的 Delphi 程序并不像您描述的那样运行,至少在我可用的 Delphi 版本 XE7 上是这样。当您的程序为 运行 时,将引发无效操作浮点异常。我将假设您实际上屏蔽了浮点异常。

更新:原来在XE7和10.3之间的某个时候,Delphi 32位codegen从fcom切换到fucom这解释了为什么 XE7 设置了 IA 浮点异常,但 10.3 没有。

您的 Delphi 代码远非最小化。让我们试着做一个真正最小的例子。让我们看看其他比较运算符。

{$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.

在 XE7 的 32 位下,输出

TRUE
TRUE
FALSE
FALSE
TRUE
FALSE

在 10.3.3 的 32 位下(以及您在下面的评论中报告的 10.4.1),此输出

TRUE
TRUE
TRUE
TRUE
FALSE
TRUE

在 XE7 和 10.3.3(以及您报告的 10.4.1)中的 64 位下,此输出

FALSE
FALSE
FALSE
FALSE
FALSE
TRUE

64位输出正确。两种变体的 32 位输出都不正确。这个我们参考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.

对于您的 32 位 Delphi 代码,您需要解决此错误并在需要处理此类比较时包含特殊情况代码。当然,除非碰巧您使用的是 10.4 并且它已经解决了问题。