问题描述
我正在将 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 生成的机器码:
MSVC2019 生成的机器码:
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 并且它已经解决了问题,否则很幸运。