64 位和 32 位 Round 之间的浮点差异

Floating point differences between 64 bit and 32 bit with Round

我知道所有关于浮点数的近似问题,所以我知道如果 4.5 被近似为 4.4999999999999991,它是如何四舍五入到 4 的。我的问题是为什么在 32 位和 64 位上使用相同的类型会有所不同。

在下面的代码中我有两个计算。在 32 位中,MyRoundValue1 的值为 4,MyRoundValue2 的值为 5。在 64 位中,它们都是 4。结果不应该与 32 位和 64 位一致吗?

{$APPTYPE CONSOLE}
const
  MYVALUE1: Double = 4.5;
  MYVALUE2: Double = 5;
  MyCalc: Double = 0.9;
var
  MyRoundValue1: Integer;
  MyRoundValue2: Integer;
begin
  MyRoundValue1 := Round(MYVALUE1);
  MyRoundValue2 := Round(MYVALUE2 * MyCalc);
  WriteLn(IntToStr(MyRoundValue1));
  WriteLn(IntToStr(MyRoundValue2));
end.

System.Round 内部接受一个 Extended 值。在 32 位中,FPU 内部的计算是 Extended。在 64 位中 Extended 类似于 Double。内部表示可能只是差异很大才能产生差异。

在 x87 中此代码:

MyRoundValue2 := Round(MYVALUE2 * MyCalc);

编译为:

MyRoundValue2 := Round(MYVALUE2 * MyCalc);
0041C4B2 DD0508E64100     fld qword ptr [[=15=]41e608]
0041C4B8 DC0D10E64100     fmul qword ptr [[=15=]41e610]
0041C4BE E8097DFEFF       call @ROUND
0041C4C3 A3C03E4200       mov [[=15=]423ec0],eax

Delphi RTL 下 x87 单元的默认控制字执行 80 位精度的计算。所以浮点数单元乘以 5 乘以 closest 64 bit value to 0.9 即:

0.90000 00000 00000 02220 44604 92503 13080 84726 33361 81640 625

请注意,此值大于 0.9。事实证明,当乘以 5 并四舍五入到最接近的 80 位值时,该值大于 4.5。因此 Round(MYVALUE2 * MyCalc) returns 5.

在 64 位上,浮点数学是在 SSE 单元上完成的。那不使用 80 位中间值。事实证明,最接近 0.9 的 double 的 5 倍,四舍五入到 double 精度恰好是 4.5。因此 Round(MYVALUE2 * MyCalc) returns 4 在 64 位上。

您可以说服 32 位编译器以与 64 位编译器相同的方式存储,而不是依赖于中间的 80 位值:

{$APPTYPE CONSOLE}
const
  MYVALUE1: Double = 4.5;
  MYVALUE2: Double = 5;
  MyCalc: Double = 0.9;
var
  MyRoundValue1: Integer;
  MyRoundValue2: Integer;
  d: Double;
begin
  MyRoundValue1 := Round(MYVALUE1);
  d := MYVALUE2 * MyCalc;
  MyRoundValue2 := Round(d);
  WriteLn(MyRoundValue1);
  WriteLn(MyRoundValue2);
end.

此程序产生与您的 64 位程序相同的输出。

或者您可以强制 x87 单元使用 64 位中间体。

{$APPTYPE CONSOLE}
uses
  SysUtils;
const
  MYVALUE1: Double = 4.5;
  MYVALUE2: Double = 5;
  MyCalc: Double = 0.9;
var
  MyRoundValue1: Integer;
  MyRoundValue2: Integer;
begin
  Set8087CW(32); //  <-- round intermediates to 64 bit
  MyRoundValue1 := Round(MYVALUE1);
  MyRoundValue2 := Round(MYVALUE2 * MyCalc);
  WriteLn(MyRoundValue1);
  WriteLn(MyRoundValue2);
end.