web-dev-qa-db-ja.com

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-Bit Server VM、Java 1.7.0_51)です)。

私の質問は、scalaバージョンの隠されたコストとは何ですか?

どうもありがとう。

55
Phil

まあ、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バージョンは2倍高速です。

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の奇妙な点が介入しましたが、この演習のポイントは、コード生成における驚くべきわずかな違いがベンチマークによって大幅に拡大されることです。

追伸@tailrec...

更新:効果のより完全な説明: http://shipilev.net/blog/2014/Java-scala-divided-we-fail/

125

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バージョンのパフォーマンスが向上した場合、テール再帰の最適化はここで効果があると思います(Javaバージョンが再帰の代わりにループ、それは再び同じように実行されます)。

23
Beryllium

私は この質問 を見て、Scalaバージョンを編集して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 one:

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

Java末尾呼び出しがないためです。最適化された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と宣言しなかったため、低速でした(直接的または間接的に Beryllium)としてanswer が指摘しています)そして、元のJavaのバージョンは、末尾呼び出しがないために遅くなりました。

7
Phil

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はメソッド呼び出しを最適化できないため、処理速度は遅くなります。

1
SpiderPig