Math.Sum Win32/64 中的不同优化

Different optimizations in Math.Sum in Win32/64

我有以下代码

const
  NumIterations = 10000000;
var
  i, j : Integer;
  x : array[1..100] of Double;
  Start : Cardinal;
  S : Double;
begin
  for i := Low(x) to High(x) do x[i] := i;

  Start := GetTickCount;
  for i := 1 to NumIterations do S := System.Math.Sum(x);
  ShowMessage('Math.Sum: ' + IntToStr(GetTickCount - Start));

  Start := GetTickCount;
  for i := 1 to NumIterations do begin
    S := 0;
    for j := Low(x) to High(x) do S := S + x[j];
  end;
  ShowMessage('Simple Sum: ' + IntToStr(GetTickCount - Start));
end;

为 Win32 编译时 Math.Sum 比简单循环快得多,因为 Math.Sum 是用汇编程序编写的,并使用四重循环展开。

但是当为 Win64 编译时,Math.Sum 比简单循环 慢得多 ,因为在 64 位中 Math.Sum 使用 Kahan 求和。这是在求和过程中最大限度地减少错误堆积的准确性优化,但比简单的循环慢得多。

即当为 Win32 编译时,我得到了针对速度优化的代码,当为 Win64 编译相同的代码时,我得到了针对准确性优化的代码。这不是我天真的期望。

Win32/64 之间的这种差异是否有合理的原因? Double 始终为 8 字节,因此 Win32/64.

中的精度应该相同

在 Delphi 的当前版本中,Math.Sum 是否仍然以相同的方式实现(Win32 中的汇编器和循环展开,Win64 中的 Kahan 求和)?我使用 Delphi-XE5.

Is Math.Sum still implemented identically (Assembler and loop unrolling in Win32, Kahan summation in Win64) in current versions of Delphi? I use Delphi-XE5.

是(Delphi10.3.2)。

Is there any sensible reason for this difference between Win32/64? Double is always 8 byte, so the accuracy should be identical in Win32/64.

32 位 Delphi for Win32 使用旧的 FPU,而 64 位编译器使用 SSE 指令。当 XE2 中引入 64 位编译器时,许多旧的汇编例程并未移植到 64 位。相反,一些例程被移植为具有与其他现代编译器类似的功能。


您可以通过引入 Kahan summation function:

来稍微增强 64 位实现
program TestKahanSum;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,Math,Diagnostics;

function KahanSum(const input : TArray<Double>): Double;
var
  sum,c,y,t : Double;
  i : Integer;         
begin
    sum := 0.0;                 
    c := 0.0;                      
    for i := Low(input) to High(input) do begin
      y := input[i] - c;  
      t := sum + y; 
      c := (t - sum) - y; 
      sum := t;                 
    end;
    Result := sum;
end;

var
  dArr : TArray<Double>;
  res : Double;
  i : Integer;
  sw : TStopWatch;
begin
  SetLength(dArr,100000000);
  for i := 0 to High(dArr) do dArr[i] := Pi;
  sw := TStopWatch.StartNew;
  res := Math.Sum(dArr);
  WriteLn('Math.Sum:',res,' [ms]:',sw.ElapsedMilliseconds);
  sw := TStopWatch.StartNew;
  res := KahanSum(dArr);
  WriteLn('KahanSum:',res,' [ms]:',sw.ElapsedMilliseconds);
  sw := TStopWatch.StartNew;
  res := 0;
  for i := 0 to High(dArr) do res := res + dArr[i];
  WriteLn('NaiveSum:',res,' [ms]:',sw.ElapsedMilliseconds);
  ReadLn;
end.

64 位:

Math.Sum: 3.14159265358979E+0008 [ms]:492
KahanSum: 3.14159265358979E+0008 [ms]:359
NaiveSum: 3.14159265624272E+0008 [ms]:246

32 位:

Math.Sum: 3.14159265358957E+0008 [ms]:67
KahanSum: 3.14159265358979E+0008 [ms]:958
NaiveSum: 3.14159265624272E+0008 [ms]:277

Pi 的 15 位数字是 3.14159265358979

在此示例中,32 位数学汇编例程精确到 13 位数字,而 64 位数学例程精确到 15 位数字。


结论:

  • 64 位实现速度较慢(与原始求和相比,慢了两倍),但比 32 位数学例程更准确。

  • 引入增强的 Kahan 求和例程可将性能提高 35%。

在切换编译目标时,具有完全相同的 RTL 函数的行为并不相同是一个可怕的错误。它不应该改变行为。更糟糕的是,Win64/pascal Sum() 在 single 或 double 上表现不一样! sum(single) 是简单的求和,而 sum(double) 使用 Kahan ... :(

您最好使用简单的 + 运算符,或者创建您自己的 Kahan 求和函数。

我可以确认该错误在 Delphi 10.3 中仍然存在。