【问题标题】:Faster sin() for x64x64 的更快 sin()
【发布时间】:2016-04-04 20:14:27
【问题描述】:

主要问题

有人有针对 x64 的快速sin() 实现吗? 它不需要是纯帕斯卡。

说明

我有一个 VCL 应用程序,当它为 x64 编译时,在某些情况下运行速度会慢很多。

它进行了大量的浮点 3d 计算,我发现这是因为当输入值变大时,System.Sin()System.Cos() 在 x64 上会慢很多。

我通过创建一个简单的测试应用程序来计时,该应用程序测量计算 sin(x) 所需的时间,x 值不同,差异很大:

              call:     x64:     x86:
              Sin(1)   16 ms    20 ms
             Sin(10)   30 ms    20 ms
            Sin(100)   32 ms    20 ms
           Sin(1000)   34 ms    21 ms
          Sin(10000)   30 ms    21 ms
         Sin(100000)   30 ms    16 ms
        Sin(1000000)   35 ms    20 ms
       Sin(10000000)  581 ms    20 ms
      Sin(100000000) 1026 ms    21 ms
     Sin(1000000000) 1187 ms    22 ms
    Sin(10000000000) 1320 ms    21 ms
   Sin(100000000000) 1456 ms    20 ms
  Sin(1000000000000) 1581 ms    17 ms
 Sin(10000000000000) 1717 ms    22 ms
Sin(100000000000000) 1846 ms    23 ms
           Sin(1E15) 1981 ms    21 ms
           Sin(1E16) 2100 ms    21 ms
           Sin(1E17) 2240 ms    22 ms
           Sin(1E18) 2372 ms    18 ms
                etc    etc      etc

您在这里看到的是 sin(1E5) 的运行速度大约是 sin(1E8) 的 300 倍。

如果您有兴趣,我已经创建了上面的表格,如下所示:

{$APPTYPE CONSOLE}
program SinTest;

uses Diagnostics, Math, SysUtils;

var
  i : Integer;
  x : double;
  sw: TStopwatch;

begin
  x := 1;

  while X < 1E18 do
  begin
    sw    := TStopwatch.StartNew;
    for i := 1 to 500000 do
      System.Sin(x);

    // WriteLn(System.sin(x), #9,System.Sin(fmod(x,2*pi)));

    sw.Stop;

    WriteLn('    ', ('Sin(' + round(x).ToString + ')'):20, ' ', sw.ElapsedMilliseconds,' ms');

    x := x * 10;
  end;

  WriteLn('Press any key to continue');
  readln;
end.

注意事项:

  • 在 StackOverflow 上有一些关于更快的正弦函数的问题,但它们都没有可用于移植到 Delphi 的源代码,例如:Fastest implementation of sine, cosine and square root in C++ (doesn't need to be much accurate)

  • x64 的其余部分比 32 位对应的运行速度更快

  • 通过执行以下操作,我发现了一些糟糕的解决方法: Sin(FMod(x,2*pi))。它提供了正确的结果,并且对于较大的数字运行得很快。当然,对于较小的数字,它会慢一些。

【问题讨论】:

  • 大概您不关心准确性,或者您不会调用具有如此大值的三角函数。您肯定明白舍入意味着三角函数对于此类输入值毫无意义吗?还是准确性对您来说并不重要?
  • 那么,看看你能不能猜出这个程序的输出:{$APPTYPE CONSOLE} var s1, s2: Single; begin s1 := 10000000.5; s2 := 10000000.0; Writeln(s1=s2); end. 这里有一个线索。输出不是FALSE
  • 似乎 MSVC 可以更快地做到这一点,我很想知道如何做到这一点,因为我敢打赌它对于合理的输入值也能更快地做到这一点。但是对于您的大输入值,您甚至调用这些三角函数都是在浪费时间,正如我之前的评论所展示的那样。
  • IMO,将 sin(fmod(x, 2 * pi)) 的代码包装到一个小函数中可能是 about 尽可能好(事实上,这是他们应该开始做的 - - 从时间上看,他们可能通过重复减法实现了fmod,如果它接近正确的范围,这很好,但如果它大大超出范围,则速度很慢并且可能不准确)。
  • 不,您使用的是单精度。它在问题中。还是这个问题不是您要问的?

标签: performance delphi 64-bit x86-64 trigonometry


【解决方案1】:

虽然这在用户模式代码中可能是相当不鼓励的(并且在内核模式代码中是完全禁止的),但如果您确实想在您的 x64 代码中保留旧的 x87 行为,您 可以写一个这样的函数:

function SinX87(x:double):double;
var
  d : double;
asm
  movsd qword ptr [rbp+8], xmm0
  fld qword ptr [rbp+8]
  fsin
  fstp qword ptr [rbp+8]
  movsd xmm0, qword ptr [rbp+8]
end;

这会增加一些开销,因为您必须将值从 SSE 寄存器弹出到堆栈中,将其加载到 x87 单元中,执行计算,将值弹出回堆栈,然后将其加载回XMM0 为函数结果。不过,sin 的计算量相当大,所以这是一个相对较小的开销。如果您需要保留 x87 的 sin 实现中的 whatever idiosyncracies,我只会这样做。

在 x64 代码中计算 sin 的效率比 Delphi 的 purepascal 例程更有效。我在这里压倒性的偏好是将一组好的 C++ 例程导出到 DLL。此外,正如大卫所说,使用带有大得离谱的参数的三角函数无论如何都不是一件明智的事情。

【讨论】:

  • 酷,无论输入什么,速度都很稳定。对于小于 pi 的值,它会慢一点;其余的总是更快。结果与 Delphi 的 System.Sin() 略有不同,但对于我需要处理的数字而言,它是微不足道的。结果看起来不错。这正是我所需要的。现在我需要做的就是添加一些丑陋的 {$ifdef} 东西,x64 下的性能就恢复了。谢谢!
  • @WoutervanNifterick 另外,我不确定如何处理异常......我肯定会先测试它。不确定 x87 控制字是否在 x64 模式下默认设置为任何合理的值 - 我很快就把它搞定了,但有一些注意事项需要注意。
  • 测试了它,确实它处理的事情有点不同。例如SinX87(NaN) 不会引发任何异常,就像 System.Sin() 那样。所以确实存在差异,但这是一个很大的帮助。我会做一些额外的测试,但到目前为止,它看起来完全按照我需要的方式完成了所有工作。
【解决方案2】:

如果您对我的最终解决方案感兴趣:

我做了一些实验,这样做(如 LU RD 和 e)。 – Jerry Coffin 建议):

function sin(x:double):double;
begin
  if x<1E6 then
    Result := system.sin(x)
  else
    Result := system.sin(fmod(x,2*pi));
end;

也许这与我的特定 CPU 上测试代码的可预测性有关,但如果我不执行 if 并且始终使用 fmod(),则实际上计算较小的值会更快。奇怪,因为需要进行一些除法,我希望这比比较两个值要慢。

所以这就是我现在最终使用的:

function sin(const x: double): double; { inline; }
begin
  {$IFDEF CPUX64}
  Result := System.sin(Math.FMod(x,2*pi));
  {$ELSE}
  Result := System.sin(x);
  {$ENDIF}
end;

顺便添加inline,它的运行速度甚至快了 1.5 倍。然后它的运行速度与我机器上 J... 的功能一样快。但即使没有 Inline,这已经比 System.Sin() 快数百倍,所以我要这样做。

【讨论】:

  • 即使您使用fmod(x, 2*pi),正如@DavidHeffernan 指出的那样,您也会遇到这样一个事实,即x 作为双精度变量,不能容纳超过17 位十进制数字的信息,所以你失去了传递给sin函数的所有精确度。例如:如果您将x 从 100000000000000000.0 步进到 100000000000000000.1,代表 0.1 弧度步进,这两个数字是相同的,因为当添加 .1 时,它会丢失,因为双精度变量不够宽抓住整个事情。你必须找到另一种方式来编码x
猜你喜欢
  • 2013-10-18
  • 1970-01-01
  • 1970-01-01
  • 2012-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-10
  • 1970-01-01
相关资源
最近更新 更多