programing

Scala의 숨겨진 성능 비용?

copysource 2021. 1. 16. 20:37
반응형

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

이것은 (Java HotSpot (TM) 64-Bit Server VM, Java 1.7.0_51)의 scala 2.10.3입니다.

내 질문은 스칼라 버전의 숨겨진 비용은 얼마입니까?

감사합니다.


음, 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의 기이 한 점이 무엇인지 알아낼 수 있지만이 연습의 요점은 코드 생성의 놀라운 미세한 차이가 벤치 마크에 의해 크게 확대된다는 것입니다.

추신 : 너무 많이 @tailrec...

업데이트 : 효과에 대한 자세한 설명 : http://shipilev.net/blog/2014/java-scala-divided-we-fail/


나는 변경 val

private val t = 20

A와 일정 정의

private final val t = 20

그리고 상당한 성능 향상을 얻었습니다. 이제 두 버전 모두 거의 동일하게 작동하는 것 같습니다. [내 시스템에서 업데이트 및 설명 참조].

나는 바이트 코드를 들여다 보지 않았지만 사용 val t = 20한다면 javap방법이 있음을 알 수 있습니다 (그 버전은를 사용하는 것만 큼 느립니다 private val).

그래서 나는 a조차도 private val메소드 호출을 포함 한다고 가정 하고 그것은 finalJava에서 와 직접 비교할 수 없습니다 .

최신 정보

내 시스템에서 다음 결과를 얻었습니다.

자바 버전 : 시간 : 14725

스칼라 버전 : 시간 : 13228

32 비트 Linux에서 OpenJDK 1.7 사용.

내 경험상 64 비트 시스템에서 Oracle의 JDK는 실제로 더 나은 성능을 발휘하므로 다른 측정 값이 Scala 버전을 선호하는 더 나은 결과를 산출한다고 설명 할 수 있습니다.

As for the Scala version performing better I assume that tail recursion optimization does have an effect here (see Phil's answer, if the Java version is rewritten to use a loop instead of recursion, it performs equally again).


I looked at this question and edited the Scala version to have t inside 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))
  }
}

The new Scala version now runs twice as fast as the original Java one:

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

I figured out it is because Java not having tail calls. The optimized Java one with loop instead of recursion runs just as fast:

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));
    }
}

Now my confusion is fully solved:

> java JavaMain
....
time: 4795

In conclusion, the original Scala version was slow because I didn't declare t to be final (directly or indirectly, as Beryllium's answer points out). And the original Java version was slow due to lack of tail calls.


To make the Java version completely equivalent to your Scala code you need to change it like this.

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);
}

It is slower because the JVM can not optimize the method calls.

ReferenceURL : https://stackoverflow.com/questions/22581163/hidden-performance-cost-in-scala

반응형