在Win32和Win64中使用exAllArithmeticExceptions出现不一致的结果

8

我的一个同事在Delphi编译Win32和Win64代码中发现了处理NaN的差异。以以下代码为例,当以32位编译时我们不会得到任何消息,但是以64位编译时我们会得到两个比较都返回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标准,<>应该返回true,但其他所有操作都应该返回false。因此,在这种情况下,看起来64位版本是正确的,而32位版本是不正确的。两者生成的代码非常不同,64位版本生成SSE代码。
对于32位:
TestNaNs.dpr.21: if nanSingle <> zeroSingle then
0041A552 D905E01E4200     fld dword ptr [$00421ee0]
0041A558 D81DE41E4200     fcomp dword ptr [$00421ee4]
0041A55E 9B               wait 
0041A55F DFE0             fstsw ax
0041A561 9E               sahf 
0041A562 7419             jz $0041a57d

而对于64位:

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

我的问题是:这是Delphi编译器的问题还是Intel CPU的一个注意事项?

{btsdaf} - David Heffernan
{btsdaf} - Graymatter
{btsdaf} - David Heffernan
{btsdaf} - Dsm
@gammatester 我已经阅读了那个问题。这里的问题是32位编译器生成的代码不符合IEEE 754标准。你所链接的问题问的是为什么NaN != NaN,这与本问题无关。 - Graymatter
显示剩余3条评论
1个回答

4
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 [$00423ec0] 0041C4CE DC1DC83E4200 fcomp qword ptr [$00423ec8] 0041C4D4 9B wait 0041C4D5 DFE0 fstsw ax 0041C4D7 9E sahf 0041C4D8 7419 jz $0041c4f3
英特尔软件开发手册表明,无序比较由标志位C3、C2和C0均设置为1表示。完整表格如下:
条件 C3 C2 C0 ST(0) > Source 0 0 0 ST(0) < Source 0 0 1 ST(0) = Source 1 0 0 无序 1 1 1
当你在调试器下检查FPU时,可以看到它的情况是这样的。
0041C4D5 DFE0 fstsw ax 0041C4D7 9E sahf 0041C4D8 7419 jz $0041c4f3
这会将FPU状态寄存器中的各种位传输到CPU标志中,精确的细节请参阅手册以了解哪些标志位应该放在哪里。如果ZF被设置,则进行分支。ZF的值来自C3 FPU标志,从上面的表格中读取,可以发现对于无序案例,它被设置。
实际上,整个分支代码可以表示为伪代码:
如果C3 = 1,则跳转
因此,从上述表格中可以明显看出,如果一个操作数是NaN,则任何浮点等号比较都将计算为相等。
64位Windows编译器
比较语句被编译为:
TestNaNs.dpr.19: 如果nanDouble不等于zeroDouble,则
0000000000428EB8 F20F100548E50000 movsd xmm0,qword ptr [rel $0000e548]
0000000000428EC0 660F2E0548E50000 ucomisd xmm0,qword ptr [rel $0000e548]
0000000000428EC8 7A02             jp TestNaNs + $5C
0000000000428ECA 7420             jz TestNaNs + $7C

比较是通过ucomisd指令执行的。 手册给出了以下伪代码:

RESULT ←无序比较(SRC1 [63:0] <> SRC2 [63:0]){
(*设置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 + $5C
0000000000428ECA 7420             jz TestNaNs + $7C

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

结论

不同的行为是由于不同的编译器在如何实现比较运算符方面做出了不同的选择。 在两种情况下,硬件都符合IEEE 754标准,并且能够根据标准比较NaN。

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

当最近创建64位编译器时,Embarcadero工程师们决定纠正其中一些错误。 他们可能认为转向新架构允许他们自由地这样做。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接