我的一位同事发现 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 的问题?
最佳答案
IEEE 754 标准定义了浮点计算的算术格式、运算、舍入规则、异常(exception)等。 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 表示。完整表格如下:
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,则任何浮点相等比较都会计算为相等。
64位Windows编译器
比较语句编译为:
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 位编译器具有相同的行为方式。
关于delphi - Win32 和 Win64 中 exAllArithmeticExceptions 的结果不一致,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46897007/