与 Win32 和 Win64 中的 exAllArithmeticExceptions 不一致的结果

Inconsistent Results with exAllArithmeticExceptions in Win32 and Win64

我的一位同事发现 Delphi 编译的 Win32 和 Win64 代码在处理 NaN 的方式上存在差异。以下面的代码为例。在 32 位编译时,我们没有收到消息,但在 64 位编译时,我们得到两个比较 returning true.

program TestNaNs;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Math;

var
  nanDouble: Double;
  zereDouble: Double;
  nanSingle: Single;
  zeroSingle: Single;
begin
  SetExceptionMask(exAllArithmeticExceptions);
  nanSingle := NaN;
  zeroSingle := 0.0;
  if nanSingle <> zeroSingle then
    WriteLn('nanSingle <> zeroSingle');

  nanDouble := NaN;
  zereDouble := 0.0;
  if nanDouble <> zereDouble then
    WriteLn('nanDouble <> zeroDouble');

  ReadLn;
end.

我对 IEEE 标准的理解是 <> 应该 return true 但所有其他操作应该 return false。所以在这种情况下,看起来 64 位版本是正确的,而 32 位版本是不正确的。两者生成的代码与64位版本生成的SSE代码有很大不同。

对于 32 位:

TestNaNs.dpr.21: if nanSingle <> zeroSingle then
0041A552 D905E01E4200     fld dword ptr [[=11=]421ee0]
0041A558 D81DE41E4200     fcomp dword ptr [[=11=]421ee4]
0041A55E 9B               wait 
0041A55F DFE0             fstsw ax
0041A561 9E               sahf 
0041A562 7419             jz [=11=]41a57d

对于 64 位:

TestNaNs.dpr.21: if nanSingle <> zeroSingle then
000000000042764E F3480F5A05C9ED0000 cvtss2sd xmm0,qword ptr [rel [=12=]00edc9]
0000000000427657 F3480F5A0DC4ED0000 cvtss2sd xmm1,qword ptr [rel [=12=]00edc4]
0000000000427660 660F2EC1         ucomisd xmm0,xmm1
0000000000427664 7A02             jp Project63 + 
0000000000427666 7420             jz Project63 + 

我的问题是这样的。这是 Delphi 编译器的问题还是英特尔 CPU 的警告?

IEEE 754 标准定义了浮点计算的算术格式、运算、舍入规则、异常等。 Delphi 编译器在可用硬件单元之上实现浮点运算。对于 32 位 Windows 编译器,这是 x87 单元,对于 64 位 Windows 编译器,这是 SSE 单元。这两个硬件单元都符合 IEEE 754 标准。

您观察到的差异出现在语言实现级别。让我们更详细地看一下这两个版本。

32 位 Windows 编译器

比较语句编译成这样:

TestNaNs.dpr.19: if nanDouble <> zeroDouble then
0041C4C8 DD05C03E4200     fld qword ptr [[=14=]423ec0]
0041C4CE DC1DC83E4200     fcomp qword ptr [[=14=]423ec8]
0041C4D4 9B               wait 
0041C4D5 DFE0             fstsw ax
0041C4D7 9E               sahf 
0041C4D8 7419             jz [=14=]41c4f3

英特尔软件开发人员手册说,无序比较由标志 C3、C2 和 C0 设置为 1 表示。完整的 table 在这里:

Condition       C3  C2  C0
ST(0) > Source  0   0   0
ST(0) < Source  0   0   1
ST(0) = Source  1   0   0
Unordered       1   1   1

当您在调试器下检查 FPU 时,您会发现情况确实如此。

0041C4D5 DFE0             fstsw ax
0041C4D7 9E               sahf 
0041C4D8 7419             jz [=16=]41c4f3

这会将 FPU 状态寄存器的各种位传输到 CPU 标志中,请参阅手册以了解哪些标志去哪里的精确细节。如果设置了 ZF,则会创建分支。 ZF 的值来自 C3 FPU 标志,从上面的 table 读取,它是为无序情况设置的。

其实整个分支代码可以用伪代码表示为:

jump if C3 = 1

所以,看看上面的 table,很明显,如果其中一个操作数是 NaN,那么任何浮点相等比较的计算结果都是相等的。

64 位 Windows 编译器

比较语句编译成这样:

TestNaNs.dpr.19: if nanDouble <> zeroDouble then
0000000000428EB8 F20F100548E50000 movsd xmm0,qword ptr [rel [=18=]00e548]
0000000000428EC0 660F2E0548E50000 ucomisd xmm0,qword ptr [rel [=18=]00e548]
0000000000428EC8 7A02             jp TestNaNs + C
0000000000428ECA 7420             jz TestNaNs + C

比较由ucomisd指令执行。手册给出了这个伪代码:

RESULT ← UnorderedCompare(SRC1[63:0] <> SRC2[63:0]) {
(* Set EFLAGS *)
CASE (RESULT) OF
  GREATER_THAN:   ZF, PF, CF ← 000;
  LESS_THAN:      ZF, PF, CF ← 001;
  EQUAL:          ZF, PF, CF ← 100;
  UNORDERED:      ZF, PF, CF ← 111;
ESAC;
OF, AF, SF ← 0;

请注意,在此指令中,ZF、PF 和 CF 标志与 x87 单元上的 C3、C2 和 C0 标志完全类似。

分支由这段代码处理:

0000000000428EC8 7A02             jp TestNaNs + C
0000000000428ECA 7420             jz TestNaNs + C

请注意,首先测试奇偶校验标志 PF(jp 指令),然后是零标志 ZF(jz 指令)。因此,编译器发出代码来处理无序情况(即其中一个操作数是 NaN)。这首先由 jp 处理。一旦处理完毕,编译器就会检查零标志 ZF(因为已经处理了 NaN)当且仅当两个操作数相等时才设置该标志。

结论

不同的行为取决于不同的编译器在如何实现比较运算符方面采取不同的选择。在这两种情况下,硬件都符合 IEEE 754 标准,并且完全能够按照标准规定比较 NaN。

我最好的猜测是 32 位编译器的决定是很久以前做出的。其中一些决定是有问题的。在我看来,无论其他操作数如何,与 NaN 操作数的相等比较都不应等于。通过保持向后兼容性的愿望感受到历史的重量,意味着这些有问题的决定从未得到解决。

最近,在创建 64 位编译器时,Embarcadero 工程师决定纠正其中的一些错误。他们大概觉得打破新架构让他们可以自由地这样做。

在理想情况下,可以通过设置编译器开关将 32 位编译器配置为与 64 位编译器的行为方式相同。