在Java中使用“sincos”

在很多情况下,我不仅需要正弦,还需要相同参数的余弦。

对于C,在常见的unix m数学库中有sincos函数。 实际上,至少在i386上,这应该是单个汇编指令fsincos

sincos,sincosf,sincosl – 同时计算sin和cos

我想这些好处是存在的,因为在计算正弦和余弦时存在明显的重叠: sin(x)^2 + cos(x)^2 = 1 。 但是AFAIK尝试将其cos = Math.sqrt(1 - sin*sin)cos = Math.sqrt(1 - sin*sin)并没有得到回报,因为sqrt函数的成本相似。

有没有办法在Java中获得相同的好处? 我想我要为double[]付出代价; 由于添加了垃圾收集,这可能使所有的努力都没有实际意义。

或者Hotspot编译器是否足够智能以识别我需要两者,并将其编译为sincos命令? 我可以测试它是否识别它,我可以帮助它识别这一点,例如确保Math.sinMath.cos命令在我的代码中直接连续吗? 从Java语言的角度来看,这实际上是最有意义的:让编译器优化它以使用fsincos汇编调用。

从一些汇编文档中收集:

 Variations 8087 287 387 486 Pentium fsin - - 122-771 257-354 16-126 NP fsincos - - 194-809 292-365 17-137 NP Additional cycles required if operand > pi/4 (~3.141/4 = ~.785) sqrt 180-186 180-186 122-129 83-87 70 NP 

fsincos应该需要额外的弹出,但这应该是1个时钟周期。 假设CPU也没有对此进行优化,那么sincos速度几乎是调用sin两倍的速度(第二次计算余弦;因此我认为它需要进行一次加法)。 sqrt在某些情况下会更快,但正弦可以更快。

更新 :我在C中做了一些实验,但它们没有定论。 有趣的是, sincos似乎甚至比sin (没有cos )更快,并且当你计算sincos时,GCC编译器将使用fsincos – 所以它做了我想要Hotspot做的事情(或者Hotspot也是如此)? )。 我不能阻止编译器通过使用fsincos来超越我,除非不使用cos 。 然后它会回落到C sin ,而不是fsin

我用卡尺执行了一些微基准测试。 在-4 * pi … 4 * pi范围内的(预先计算的)随机数arrays上进行10000000次迭代。 我尽力获得最快的JNI解决方案 – 我有点难以预测你是否会真正获得fsincos或一些模拟的fsincos 。 报告的数字是10个卡尺试验中最好的(其中包括3-10个试验,其中报告的平均值)。 因此粗略地说,每个内循环运行30-100次。

我已经对几种变体进行了基准测试:

  • 仅限Math.sin (参考)
  • Math.cos (参考)
  • Math.sin + Math.cos
  • 通过JNI的sincos
  • Math.sin + cos通过Math.sqrt( (1+sin) * (1-sin) ) +符号重建
  • Math.cos + sin通过Math.sqrt( (1+cos) * (1-cos) ) +符号重建

(1+sin)*(1-sin)=1-sin*sin数学上,但如果sin接近1,它应该更精确? 运行时差异很小,您可以保存一个添加项。

通过x %= TWOPI; if (x<0) x+=TWOPI;符号重建x %= TWOPI; if (x<0) x+=TWOPI; x %= TWOPI; if (x<0) x+=TWOPI; 然后检查象限。 如果你知道如何用更少的CPU来做到这一点,我会很高兴听到。

通过sqrt数值损失似乎没问题,至少对于常见角度而言。 在粗糙实验的1e-10范围内。

 Sin 1,30 ============== Cos 1,29 ============== Sin, Cos 2,52 ============================ JNI sincos 1,77 =================== SinSqrt 1,49 ================ CosSqrt 1,51 ================ 

sqrt(1-s*s)sqrt((1+s)*(1-s))差异大约为0.01。 正如您所看到的,基于sqrt的方法可以胜过任何其他方法(因为我们目前无法访问纯Java中的sincos )。 JNI sincos比计算sincos更好,但sqrt方法仍然更快。 cos本身似乎始终是一个优于sin的刻度(0,01),但重构符号的情况区别有一个额外的>测试。 我不认为我的结果支持sin+sqrtcos+sqrt显然是可取的,但与sin然后cos相比,它们确实节省了大约40%的时间。

如果我们将Java扩展为具有内在优化的sincos ,那么这可能会更好。 恕我直言,这是一个常见的用例,例如图形。 当在AWT,Batik等中使用时,许多应用程序都可以从中受益。

如果我再次运行它,我还会添加JNI sinnoop来估算JNI的成本。 也许还可以通过JNI对sqrt技巧进行基准测试。 只是为了确保从长远来看我们确实需要一个内在的sincos

大多数sin和cos计算是直接调用硬件。 没有比这更快的计算方法了。 具体而言,在+ -pi / 4的范围内,速率非常快。 如果您通常使用硬件加速,并尝试将值限制为指定的值,那么您应该没问题。 来源 。

查看Hotspot代码,我相信Oracle Hotspot VM不会将sin(a)+ cos(a)优化为fsincos:请参阅assembler_x86.cpp ,第7482ff行。

但是,我怀疑单独使用fsin和fcos的机器周期数增加很容易被运行GC等其他操作所掩盖。 我会使用标准的Javafunction并分析应用程序。 只有当一个配置文件运行表明在sin / cos调用中花费了大量时间时,我才会冒险去做一些事情。

在这种情况下,我将创建一个使用2元素jdoublearray作为out参数的JNI包装器。 如果您只有一个使用sincos JNI操作的线程,则可以在Java代码中使用静态初始化的double [2]数组,该数组将被重复使用。

你可以随时介绍。

但是,一般来说,sqrt应该与division相同,因为div和sqrt的内部实现非常相似。

Sin和余弦,OTOH使用高达10度的多项式计算,没有任何公共系数,并且可能是难以模2pi减少 – 这是sincos中共享的唯一公共部分(当不使用CORDIC时)。

编辑修改后的配置文件(修正了拼写错误)显示了时间差异

 sin+cos: 1.580 1.580 1.840 (time for 200M iterations, 3 successive trials) sincos: 1.080 0.900 0.920 sin+sqrt: 0.870 1.010 0.860 

常规Java中没有可用的fsincos 。 此外,JNI版本可能比对java.lang.Math.sin()和cos()的双重调用慢。

我猜你关心sin(x)/ cos(x)的速度。 所以我给你一个快速三角运算的建议,取代fsincos:Look Up Table。 以下是我的原帖。 我希望它对你有所帮助。

=====

我尝试使用查找表(LUT)在三角函数(sin和cos)上实现最佳性能。

我发现了什么:

  • LUT可以比java.lang.Math.sin()/ cos()快20-25倍 。 可能与原生fsin / fcos一样快。 也许和fsincos一样快。
  • 但是,如果使用0到45度之间的角度,java.lang.Math.sin()和cos()比任何其他计算sin / cos的方式更快 ;
  • 但请注意,低于12度的角度sin(x)几乎== x。 它甚至更快;

  • 一些实现使用float数组来存储sin,另一个实现使用cos。 这是不必要的。 请记住:

cos(x) == sin(x + PI/2)

  • 也就是说,如果你有sin(x)表,你可以免费获得cos(x)表。

我使用java.lang.Math.sin()对sin() 范围内的角度进行了一些测试[0..45] 。 一个用于360个位置的天真查找表,一个优化的LUT90,其表值为[0..90],但扩展为[0..360]; 并查找带插值的表。注意警告后,java.lang.Math.sin()比其他更快:

 Size test: 10000000 Angles range: [0.0...45.0] Time in ms Trial | Math.sin() | Lut sin() | LUT90.sin() | Lut sin2() [interpolation] 0 312,5879 25,2280 27,7313 36,4127 1 12,9468 19,5467 21,9396 34,2344 2 7,6811 16,7897 18,9646 32,5473 3 7,7565 16,7022 19,2343 32,8700 4 7,6634 16,9498 19,6307 32,8087 

这里有资料GitHub

但是,如果你需要在范围[-360..360]中的高性能,java.lang.Math lib会更慢。 查找表(LUT)快20倍左右。 如果需要高精度,可以使用LUT进行插值,它比java.lang.Math慢一点但速度更快。 在上面的链接上看到我在Math2.java中的sin2()。

以下数字适用于角度高范围:

 Size test: 10000000 Angles range: [-360.0...360.0] Time in ms Trial|Math.sin() | Lut sin() | LUT90.sin() | Lut.sin2() [interpolation] 0 942,7756 35,1488 47,4198 42,9466 1 915,3628 28,9924 37,9051 41,5299 2 430,3372 24,8788 34,9149 39,3297 3 428,3750 24,8316 34,5718 39,5187