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

问题描述

我正在将 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] := $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.

我已经标记了比较后执行的行。 当 lon12 变量等于 +NAN 时,Delphi 将 (lon12 >= 0) 评估为 TRUE。 当 lon12 变量等于 +NAN 时,MSVC 将 (lon12 >= 0) 评估为 FALSE。

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

anground 接收 +NAN 作为参数返回不同的值。

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

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

Delphi 生成的机器码:

Delphi generated machinecode

MSVC2019 生成的机器码:

MSVC2019 generated machine code

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

解决方法

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

更新:事实证明,在 XE7 和 10.3 之间的某个时间,Delphi 32 位代码生成器从 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(和 10.4.1,正如您在下面的评论中报告的那样)中的 32 位下,此输出

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?

看到这一点

所有与运算符 ==、=、 的比较,其中一个或两个值为 NaN 都返回 false,这与所有其他值的行为相反。

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