我遇到了这个old question并使用scala 2.10.3进行了以下实验。
我重写了Scala版本以使用显式尾递归:
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
这是scala 2.10.3 on(Java HotSpot(TM)64位服务器VM,Java 1.7.0_51)。
我的问题是scala版本隐藏的成本是多少?
非常感谢。
答案 0 :(得分:125)
那么,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();
}
}
...这是示例Scala基准测试:
@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
o.s.ScalaBench.test 1 avgt 15 0.005 0.000 us/op
o.s.ScalaBench.test 5 avgt 15 0.489 0.001 us/op
o.s.ScalaBench.test 10 avgt 15 23.672 0.087 us/op
o.s.ScalaBench.test 15 avgt 15 3406.492 9.239 us/op
o.s.ScalaBench.test 20 avgt 15 2483221.694 5973.236 us/op
Benchmark (t) Mode Samples Mean Mean error Units
o.s.JavaBench.test 1 avgt 15 0.002 0.000 us/op
o.s.JavaBench.test 5 avgt 15 0.254 0.007 us/op
o.s.JavaBench.test 10 avgt 15 12.578 0.098 us/op
o.s.JavaBench.test 15 avgt 15 1628.694 11.282 us/op
o.s.JavaBench.test 20 avgt 15 1066113.157 11274.385 us/op
请注意,我们调查t
以查看效果是否为t
的特定值的本地效果。它不是,效果是系统的,Java版本的速度是其两倍。
PrintAssembly将对此有所了解。这是Scala基准测试中最热门的一块:
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&#39; s Delight,Ch.10,Sect.19)。当编译器检测到我们根据常量计算余数时,这是可能的,这表明Java版本达到了甜蜜的优化,但Scala版本却没有。您可以深入研究字节码反汇编,以找出scalac中干预的怪癖,但本练习的重点是代码生成中令人惊讶的微小差异被基准放大了很多。
P.S。 @tailrec
......
更新:对效果的更全面的解释:http://shipilev.net/blog/2014/java-scala-divided-we-fail/
答案 1 :(得分:23)
我更改了val
private val t = 20
到常量定义
private final val t = 20
并获得了显着的性能提升,现在似乎两个版本的表现几乎相同[在我的系统上,请参阅更新和评论]。
我没有研究字节码,但是如果你使用val t = 20
,你可以看到使用javap
有一个方法(而且那个版本和{{1}一样慢}})。
所以我假设即使private val
涉及调用方法,也不能与Java中的private val
直接比较。
<强>更新强>
在我的系统上,我得到了这些结果
Java版:时间:14725
Scala版本:时间:13228
在32位Linux上使用OpenJDK 1.7。
根据我的经验,Oracle 64位系统上的JDK实际上表现更好,因此这可能解释了其他测量结果更有利于Scala版本。
至于Scala版本表现更好我假设尾递归优化确实在这里产生影响(参见Phil的答案,如果Java版本被重写为使用循环而不是递归,它会再次执行)。
答案 2 :(得分:7)
我查看了this question并修改了Scala版本,以便t
内有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没有尾调用。优化的Java with loop而不是递归运行速度同样快:
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版本很慢,因为我没有将t
声明为final
(直接或间接地,Beryllium的answer指出)。由于缺少尾调用,原始的Java版本很慢。
答案 3 :(得分:1)
要使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无法优化方法调用。