隐藏在Scala中的性能成本?

我遇到了这个老问题 ,并用scala 2.10.3做了下面的实验。

我重写了Scala版本以使用明确的尾recursion:

import scala.annotation.tailrec object ScalaMain { private val t = 20 private def run() { var i = 10 while(!isEvenlyDivisible(2, i, t)) i += 2 println(i) } @tailrec private def isEvenlyDivisible(i: Int, a: Int, b: Int): Boolean = { if (i > b) true else (a % i == 0) && isEvenlyDivisible(i+1, a, b) } def main(args: Array[String]) { val t1 = System.currentTimeMillis() var i = 0 while (i < 20) { run() i += 1 } val t2 = System.currentTimeMillis() println("time: " + (t2 - t1)) } } 

并将其与以下Java版本进行比较。 我有意识地把这个函数做成非静态的,与Scala进行公平的比较:

 public class JavaMain { private final int t = 20; private void run() { int i = 10; while (!isEvenlyDivisible(2, i, t)) i += 2; System.out.println(i); } private boolean isEvenlyDivisible(int i, int a, int b) { if (i > b) return true; else return (a % i == 0) && isEvenlyDivisible(i+1, a, b); } public static void main(String[] args) { JavaMain o = new JavaMain(); long t1 = System.currentTimeMillis(); for (int i = 0; i < 20; ++i) o.run(); long t2 = System.currentTimeMillis(); System.out.println("time: " + (t2 - t1)); } } 

以下是我的电脑上的结果:

 > java JavaMain .... time: 9651 > scala ScalaMain .... time: 20592 

(Java HotSpot(TM)64位服务器VM,Java 1.7.0_51)上的scala 2.10.3。

我的问题是scala版本的隐藏成本是多less?

非常感谢。

那么,OP的基准是不是理想的。 很多效果需要减轻,包括热身,死代码消除,分叉等。幸运的是, JMH已经处理了很多事情,并且对Java和Scala都有绑定。 请按照JMH页面上的程序来获取基准项目,然后您可以移植下面的基准。

这是Java基准的示例:

 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Benchmark) @Fork(3) @Warmup(iterations = 5) @Measurement(iterations = 5) public class JavaBench { @Param({"1", "5", "10", "15", "20"}) int t; private int run() { int i = 10; while(!isEvenlyDivisible(2, i, t)) i += 2; return i; } private boolean isEvenlyDivisible(int i, int a, int b) { if (i > b) return true; else return (a % i == 0) && isEvenlyDivisible(i + 1, a, b); } @GenerateMicroBenchmark public int test() { return run(); } } 

…这是斯卡拉样本的基准:

 @BenchmarkMode(Array(Mode.AverageTime)) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Benchmark) @Fork(3) @Warmup(iterations = 5) @Measurement(iterations = 5) class ScalaBench { @Param(Array("1", "5", "10", "15", "20")) var t: Int = _ private def run(): Int = { var i = 10 while(!isEvenlyDivisible(2, i, t)) i += 2 i } @tailrec private def isEvenlyDivisible(i: Int, a: Int, b: Int): Boolean = { if (i > b) true else (a % i == 0) && isEvenlyDivisible(i + 1, a, b) } @GenerateMicroBenchmark def test(): Int = { run() } } 

如果你在JDK 8 GA,Linux x86_64上运行这些,那么你会得到:

 Benchmark (t) Mode Samples Mean Mean error Units osScalaBench.test 1 avgt 15 0.005 0.000 us/op osScalaBench.test 5 avgt 15 0.489 0.001 us/op osScalaBench.test 10 avgt 15 23.672 0.087 us/op osScalaBench.test 15 avgt 15 3406.492 9.239 us/op osScalaBench.test 20 avgt 15 2483221.694 5973.236 us/op Benchmark (t) Mode Samples Mean Mean error Units osJavaBench.test 1 avgt 15 0.002 0.000 us/op osJavaBench.test 5 avgt 15 0.254 0.007 us/op osJavaBench.test 10 avgt 15 12.578 0.098 us/op osJavaBench.test 15 avgt 15 1628.694 11.282 us/op osJavaBench.test 20 avgt 15 1066113.157 11274.385 us/op 

请注意,我们玩弄了t的特定值是否是局部的。 不是,效果是系统化的,Java版本快两倍。

PrintAssembly将揭示这一点。 这是Scala基准testing中最热门的部分:

 0x00007fe759199d42: test %r8d,%r8d 0x00007fe759199d45: je 0x00007fe759199d76 ;*irem ; - org.sample.ScalaBench::isEvenlyDivisible@11 (line 52) ; - org.sample.ScalaBench::run@10 (line 45) 0x00007fe759199d47: mov %ecx,%eax 0x00007fe759199d49: cmp $0x80000000,%eax 0x00007fe759199d4e: jne 0x00007fe759199d58 0x00007fe759199d50: xor %edx,%edx 0x00007fe759199d52: cmp $0xffffffffffffffff,%r8d 0x00007fe759199d56: je 0x00007fe759199d5c 0x00007fe759199d58: cltd 0x00007fe759199d59: idiv %r8d 

…这是类似于Java的块:

 0x00007f4a811848cf: movslq %ebp,%r10 0x00007f4a811848d2: mov %ebp,%r9d 0x00007f4a811848d5: sar $0x1f,%r9d 0x00007f4a811848d9: imul $0x55555556,%r10,%r10 0x00007f4a811848e0: sar $0x20,%r10 0x00007f4a811848e4: mov %r10d,%r11d 0x00007f4a811848e7: sub %r9d,%r11d ;*irem ; - org.sample.JavaBench::isEvenlyDivisible@9 (line 63) ; - org.sample.JavaBench::isEvenlyDivisible@19 (line 63) ; - org.sample.JavaBench::run@10 (line 54) 

请注意,在Java版本中,编译器采用了将整数余数计算转换为乘法和移位权的技巧(参见Hacker's Delight,第10章,第19节)。 这是可能的,当编译器检测到我们计算对常量的余数,这表明Java版本击中甜蜜的优化,但斯卡拉版本没有。 你可以深入研究字节码的反汇编问题,找出scalac中存在的问题,但是这个练习的重点在于代码生成中令人惊讶的微小差异被基准放大了很多。

PS这么多的@tailrec

更新:对效果更全面的解释: http : //shipilev.net/blog/2014/java-scala-divided-we-fail/

我改变了val

 private val t = 20 

到一个不变的定义

 private final val t = 20 

并获得了显着的性能提升,现在看起来两个版本的性能差不多,在我的系统上,请参阅更新和评论。

我没有看过字节码,但是如果你使用val t = 20你可以看到使用javap有一个方法(和那个版本一样慢的private val )。

所以我认为即使是一个private val需要调用一个方法,这与Java中的final一个方法不能直接比较。

更新

在我的系统上,我得到了这些结果

Java版本:时间:14725

斯卡拉版本:时间:13228

在32位Linux上使用OpenJDK 1.7。

根据我的经验,64位系统上的Oracle JDK确实执行得更好,所以这也许可以解释为了支持Scala版本,其他测量结果会产生更好的结果。

至于Scala版本performance更好,我认为尾recursion优化在这里确实有效果(参见Phil的回答,如果Java版本被重写为使用循环而不是recursion,它会再次执行)。

我看了这个问题 ,编辑了Scala版本以便run

 object ScalaMain { private def run() { val t = 20 var i = 10 while(!isEvenlyDivisible(2, i, t)) i += 2 println(i) } @tailrec private def isEvenlyDivisible(i: Int, a: Int, b: Int): Boolean = { if (i > b) true else (a % i == 0) && isEvenlyDivisible(i+1, a, b) } def main(args: Array[String]) { val t1 = System.currentTimeMillis() var i = 0 while (i < 20) { run() i += 1 } val t2 = System.currentTimeMillis() println("time: " + (t2 - t1)) } } 

新的Scala版本的运行速度是原来的Java版本的两倍:

 > fsc ScalaMain.scala > scala ScalaMain .... time: 6373 > fsc -optimize ScalaMain.scala .... time: 4703 

我想通了,这是因为Java没有尾巴呼叫。 循环而不是recursion的优化Java运行速度一样快:

 public class JavaMain { private static final int t = 20; private void run() { int i = 10; while (!isEvenlyDivisible(i, t)) i += 2; System.out.println(i); } private boolean isEvenlyDivisible(int a, int b) { for (int i = 2; i <= b; ++i) { if (a % i != 0) return false; } return true; } public static void main(String[] args) { JavaMain o = new JavaMain(); long t1 = System.currentTimeMillis(); for (int i = 0; i < 20; ++i) o.run(); long t2 = System.currentTimeMillis(); System.out.println("time: " + (t2 - t1)); } } 

现在我的困惑已经完全解决了:

 > java JavaMain .... time: 4795 

总之,最初的Scala版本很慢,因为我没有宣布tfinal (直接或间接地,因为铍的答案指出)。 原始的Java版本由于缺less尾部调用而变得很慢。

要使Java版本完全等同于您的Scala代码,您需要像这样更改它。

 private int t = 20; private int t() { return this.t; } private void run() { int i = 10; while (!isEvenlyDivisible(2, i, t())) i += 2; System.out.println(i); } 

由于JVM无法优化方法调用,所以速度较慢。