簡體   English   中英

Scala的隱藏性能成本?

[英]Hidden performance cost in Scala?

我遇到了這個老問題 ,並使用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版本的隱藏成本是多少?

非常感謝。

那么,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's Delight,Ch.10,Sect.19)。 當編譯器檢測到我們根據常量計算余數時,這是可能的,這表明Java版本達到了甜蜜的優化,但Scala版本卻沒有。 您可以深入研究字節碼反匯編以找出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

Scala版本:時間:13228

在32位Linux上使用OpenJDK 1.7。

根據我的經驗,64位系統上的Oracle JDK實際上表現更好,因此這可能解釋了其他測量結果會產生更好的結果,有利於Scala版本。

至於Scala版本表現更好我假設尾遞歸優化確實在這里產生影響(參見Phil的答案,如果Java版本被重寫為使用循環而不是遞歸,它會再次執行)。

我看了看這個問題和編輯的斯卡拉版本有trun

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

總之,最初的版本斯卡拉是緩慢的,因為我沒有申報tfinal (直接或間接地為答案指出)。 由於缺少尾調用,原始Java版本很慢。

要使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無法優化方法調用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM