我的一位同事发现了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标准的理解是&lt;&gt;应返回true,但所有其他操作应返回false。所以在这种情况下,看起来64位版本是正确的,32位版本是不正确的。两者生成的代码与生成SSE代码的64位版本非常不同。
对于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的警告?
答案 0 :(得分:4)
IEEE 754标准定义了浮点计算的算术格式,运算,舍入规则,异常等。 Delphi编译器在可用的硬件单元之上实现浮点运算。对于32位Windows编译器,这是x87单元,对于64位Windows编译器,这是SSE单元。这两个硬件单元都符合IEEE 754标准。
您正在观察的差异出现在语言实现级别。让我们更详细地看看这两个版本。
比较语句编译为:
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表示无序比较。完整的表格在这里:
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 $0041c4f3
这会将FPU状态寄存器中的各个位传输到CPU标志中,有关哪些标志位于何处的详细信息,请参阅手册。如果设置了ZF,则创建分支。 ZF的值来自C3 FPU标志,从上表中读取,为无序情况设置。
实际上,整个分支代码可以用伪代码表示为:
jump if C3 = 1
所以,看看上面的表格,很明显,如果其中一个操作数是NaN,那么任何浮点相等比较都会评估为等于。
比较语句编译为:
TestNaNs.dpr.19: if nanDouble <> zeroDouble then 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 ← 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 + $5C 0000000000428ECA 7420 jz TestNaNs + $7C
请注意,首先测试奇偶校验标志PF(jp
指令),然后是零标志ZF(jz
指令)。因此,编译器发出代码来处理无序情况(即其中一个操作数是NaN)。首先使用jp
处理此问题。一旦处理完毕,编译器就会检查零标志ZF(因为已经处理过NaN),当且仅当两个操作数相等时才会被设置。
不同的行为取决于不同的编译器在如何实现比较运算符方面做出不同的选择。在这两种情况下,硬件都符合IEEE 754标准,完全能够比较标准规定的NaN。
我最好的猜测是32位编译器的决定是在很久以前的。其中一些决定值得怀疑。在我看来,与NaN操作数的等式比较应该评估不等于其他操作数。通过保持向后兼容性的愿望感受到历史的重要性,这意味着这些可疑的决定从未得到解决。
当64位编译器创建时,最近,Embarcadero工程师决定纠正其中一些错误。他们可能认为,对新建筑的突破让他们可以自由地这样做。
在理想情况下,通过设置编译器开关,可以将32位编译器配置为与64位编译器的行为相同。