为什么用于斐波那契计算的简单Scala tailrec循环比Java循环快3倍?
Scala 代码: 字节码:为什么用于斐波那契计算的简单Scala tailrec循环比Java循环快3倍?,java,scala,tail-recursion,jmh,Java,Scala,Tail Recursion,Jmh,Scala 代码: 字节码: private long fastLoop(int, long, long); Code: 0: iload_1 1: iconst_1 2: if_icmple 21 5: iload_1 6: iconst_1 7: isub 8: lload 4 10: lload_2 11: lload
private long fastLoop(int, long, long);
Code:
0: iload_1
1: iconst_1
2: if_icmple 21
5: iload_1
6: iconst_1
7: isub
8: lload 4
10: lload_2
11: lload 4
13: ladd
14: lstore 4
16: lstore_2
17: istore_1
18: goto 0
21: lload 4
23: lreturn
private long fastLoop(int, long, long);
Code:
0: iload_1
1: iconst_1
2: if_icmple 24
5: lload_2
6: lload 4
8: ladd
9: lstore 6
11: lload 4
13: lstore_2
14: lload 6
16: lstore 4
18: iinc 1, -1
21: goto 0
24: lload 4
26: lreturn
结果是53879289.462±6289454.961操作/s
:
Java
代码:
字节码:
private long fastLoop(int, long, long);
Code:
0: iload_1
1: iconst_1
2: if_icmple 21
5: iload_1
6: iconst_1
7: isub
8: lload 4
10: lload_2
11: lload 4
13: ladd
14: lstore 4
16: lstore_2
17: istore_1
18: goto 0
21: lload 4
23: lreturn
private long fastLoop(int, long, long);
Code:
0: iload_1
1: iconst_1
2: if_icmple 24
5: lload_2
6: lload 4
8: ladd
9: lstore 6
11: lload 4
13: lstore_2
14: lload 6
16: lstore 4
18: iinc 1, -1
21: goto 0
24: lload 4
26: lreturn
结果是17444340.812±9508030.117次/s
:
是的,这取决于环境参数(JDK版本、CPU型号和RAM频率)和动态状态。但是,为什么在相同的环境中,几乎相同的字节码可以为函数参数的范围产生稳定的2x-3x差异
以下是我的笔记本电脑中不同函数参数值的ops/s编号列表,该笔记本电脑采用Intel(R)Core(TM)i7-2640M CPU@2.80GHz(最大3.50GHz)、RAM 12Gb DDR3-1333、Ubuntu 14.10、Oracle JDK 1.8.0_40-b25 64位:
[info] Benchmark (n) Mode Cnt Score Error Units
[info] JavaFibonacci.loop 2 thrpt 5 171776163.027 ± 4620419.353 ops/s
[info] JavaFibonacci.loop 4 thrpt 5 144793748.362 ± 25506649.671 ops/s
[info] JavaFibonacci.loop 8 thrpt 5 67271848.598 ± 15133193.309 ops/s
[info] JavaFibonacci.loop 16 thrpt 5 54552795.336 ± 17398924.190 ops/s
[info] JavaFibonacci.loop 32 thrpt 5 41156886.101 ± 12905023.289 ops/s
[info] JavaFibonacci.loop 64 thrpt 5 24407771.671 ± 4614357.030 ops/s
[info] ScalaFibonacci.loop 2 thrpt 5 148926292.076 ± 23673126.125 ops/s
[info] ScalaFibonacci.loop 4 thrpt 5 139184195.527 ± 30616384.925 ops/s
[info] ScalaFibonacci.loop 8 thrpt 5 109050091.514 ± 23506756.224 ops/s
[info] ScalaFibonacci.loop 16 thrpt 5 81290743.288 ± 5214733.740 ops/s
[info] ScalaFibonacci.loop 32 thrpt 5 38937420.431 ± 8324732.107 ops/s
[info] ScalaFibonacci.loop 64 thrpt 5 22641295.988 ± 5961435.507 ops/s
另外一个问题是“为什么ops/s的值会以上述非线性方式下降?”是的,我错了,错过了测试方法不仅仅是
快速循环调用:
Scala
@Benchmark
def loop(): BigInt =
if (n > 92) loop(n - 91, 4660046610375530309L, 7540113804746346429L)
else fastLoop(n)
@Benchmark
public BigInteger loop() {
return n > 92 ?
loop(n - 91, BigInteger.valueOf(4660046610375530309L), BigInteger.valueOf(7540113804746346429L)) :
BigInteger.valueOf(fastLoop(n, 0, 1));
}
Java
@Benchmark
def loop(): BigInt =
if (n > 92) loop(n - 91, 4660046610375530309L, 7540113804746346429L)
else fastLoop(n)
@Benchmark
public BigInteger loop() {
return n > 92 ?
loop(n - 91, BigInteger.valueOf(4660046610375530309L), BigInteger.valueOf(7540113804746346429L)) :
BigInteger.valueOf(fastLoop(n, 0, 1));
}
正如Aleksey所指出的,很多时间都花在从Long/Long
到BigInt/biginger
的转换上
我编写了单独的基准测试,只测试fastLoop(n,0,1)
call。以下是它们的结果:
[info] JavaFibonacci.fastLoop 2 thrpt 5 338071686.910 ± 66146042.535 ops/s
[info] JavaFibonacci.fastLoop 4 thrpt 5 231066635.073 ± 3702419.585 ops/s
[info] JavaFibonacci.fastLoop 8 thrpt 5 174832245.690 ± 36491363.939 ops/s
[info] JavaFibonacci.fastLoop 16 thrpt 5 95162799.968 ± 16151609.596 ops/s
[info] JavaFibonacci.fastLoop 32 thrpt 5 60197918.766 ± 10662747.434 ops/s
[info] JavaFibonacci.fastLoop 64 thrpt 5 29564087.602 ± 3610164.011 ops/s
[info] ScalaFibonacci.fastLoop 2 thrpt 5 336588218.560 ± 56762496.725 ops/s
[info] ScalaFibonacci.fastLoop 4 thrpt 5 224918874.670 ± 35499107.133 ops/s
[info] ScalaFibonacci.fastLoop 8 thrpt 5 121952667.394 ± 17314931.711 ops/s
[info] ScalaFibonacci.fastLoop 16 thrpt 5 96573968.960 ± 12757890.175 ops/s
[info] ScalaFibonacci.fastLoop 32 thrpt 5 59462408.940 ± 14924369.138 ops/s
[info] ScalaFibonacci.fastLoop 64 thrpt 5 28922994.377 ± 7209467.197 ops/s
我学到的教训:
- Scala隐含着它可以吃掉很多性能,但容易被忽略
- 与Java的BigInteger相比,在Scala中兑现BigInt值可以加快某些函数的速度
您必须首先检查字节码。这是一个非常相似的问题,不同之处在于隐式循环展开。你能详细介绍一下你使用的bechmarking机制吗?看起来你的测量技术有问题的可能性要比实际速度慢的可能性大得多。你读过这篇文章吗?JVM的字节码不会影响性能,您必须重复本文中的所有研究。请提供stack和perf Profiler输出。还请注意错误相当大。如果您使用“适当”的单位,比如“ops/us”,那么就可以清楚地看到上表中Java/Scala结果的大多数差异都不显著。您还必须跟踪调用快速循环前后发生的情况。我为您的GitHub项目挖掘了数据,在我的机器上,n=10时,只有20%的CPU时间用于fastLoop
,其余80%用于处理BigInt
/biginger
。请使用“适当”单位,例如“ops/us”。太难读了谢谢你,伊万!我已切换到ops/ms(因为ops/us报告过于粗糙,如~10^-4)