Scala中隐藏的性能成本?

2022-08-31 17:44:14

我遇到了这个古老的问题,并用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-Bit Server VM, Java 1.7.0_51)。

我的问题是Scala版本的隐性成本是多少?

非常感谢。


答案 1

好吧,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

请注意,我们进行杂耍,以查看效果对于 的特定值是否是局部的。它不是,效果是系统的,Java版本是两倍快。tt

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's Delight,第10章,第19节)。当编译器检测到我们根据常量计算余数时,这是可能的,这表明Java版本达到了甜蜜的优化,但Scala版本没有。你可以深入研究字节码的反汇编,找出scalac中的哪些怪癖介入了,但这个练习的重点是,代码生成中令人惊讶的微小差异被基准测试放大了很多。

附言:这么多...@tailrec

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


答案 2

我更改了val

private val t = 20

常量定义

private final val t = 20

并获得了显着的性能提升,现在似乎两个版本的性能几乎相同[在我的系统上,请参阅更新和评论]。

我还没有研究过字节码,但是如果你使用,你可以看到有一个方法(并且该版本与带有)val t = 20javapprivate val

因此,我假设即使是 a 也涉及调用方法,这与 Java 中的 a 没有直接的可比性。private valfinal

更新

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

Java 版本:时间:14725

Scala 版本: 时间: 13228

在 32 位 Linux 上使用 OpenJDK 1.7。

根据我的经验,Oracle在64位系统上的JDK实际上确实表现得更好,所以这可能解释了其他测量结果比Scala版本更好。

至于Scala版本表现得更好,我认为尾递归优化在这里确实有影响(参见Phil的答案,如果Java版本被重写为使用循环而不是递归,它再次执行相同)。


推荐