[java] 스칼라에서 이해력과 루프를 최적화하는 방법은 무엇입니까?

스칼라는 자바만큼 빠르다. 스칼라에서 원래 Java에서 해결 한 일부 프로젝트 오일러 문제를 다시 방문하고 있습니다. 구체적으로 문제 5 : “1에서 20까지의 모든 숫자로 균등하게 나눌 수있는 가장 작은 양수는 무엇입니까?”

내 Java 솔루션은 내 컴퓨터에서 완료하는 데 0.7 초가 걸립니다.

public class P005_evenly_divisible implements Runnable{
    final int t = 20;

    public void run() {
        int i = 10;
        while(!isEvenlyDivisible(i, t)){
            i += 2;
        }
        System.out.println(i);
    }

    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) {
        new P005_evenly_divisible().run();
    }
}

스칼라로의 “직접 번역”은 103 초 (147 배 더 깁니다!)입니다.

object P005_JavaStyle {
    val t:Int = 20;
    def run {
        var i = 10
        while(!isEvenlyDivisible(i,t))
            i += 2
        println(i)
    }
    def isEvenlyDivisible(a:Int, b:Int):Boolean = {
        for (i <- 2 to b)
            if (a % i != 0)
                return false
        return true
    }
    def main(args : Array[String]) {
        run
    }
}

마지막으로 함수 프로그래밍에 대한 나의 시도는 39 초 (55 배 더 길다)

object P005 extends App{
    def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
    def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
    println (find (2))
}

Windows 7 64 비트에서 Scala 2.9.0.1 사용 성능을 개선하려면 어떻게합니까? 내가 뭔가 잘못하고 있습니까? 아니면 Java가 훨씬 더 빠릅니까?



답변

이 특정한 경우의 문제점은 for-expression 내에서 리턴한다는 것입니다. 결과적으로 NonLocalReturnException이 발생하여 엔 클로징 메소드에서 발견됩니다. 옵티마이 저는 foreach를 제거 할 수 있지만 아직 throw / catch를 제거 할 수는 없습니다. 던지기 / 캐치는 비싸다. 그러나 스칼라 프로그램에서는 이러한 중첩 수익이 드물기 때문에 옵티마이 저는 아직이 경우를 다루지 않았습니다. 이 문제를 곧 해결할 수있는 최적화 프로그램을 개선하기위한 작업이 진행 중입니다.


답변

문제는 아마도 for그 방법에서 이해력을 사용하는 것 isEvenlyDivisible입니다. for동등한 while루프로 교체 하면 Java와의 성능 차이가 없어집니다.

자바의 for루프와 는 달리 , 스칼라의 for이해는 실제로 고차 방법의 구문 설탕이다. 이 경우 객체 에서 foreach메소드를 호출 Range합니다. 스칼라 for는 매우 일반적이지만 때로는 고통스러운 성능으로 이어집니다.

-optimizeScala 버전 2.9에서 플래그 를 사용해 볼 수 있습니다 . 관찰 된 성능은 사용중인 특정 JVM 및 핫스팟을 식별하고 최적화하기에 충분한 “웜업”시간이있는 JIT 최적화 프로그램에 따라 달라질 수 있습니다.

메일 링리스트에 대한 최근 토론에 따르면 스칼라 팀은 for간단한 경우에 성능 향상을 위해 노력 하고 있습니다.

다음은 버그 추적기의 문제입니다.
https://issues.scala-lang.org/browse/SI-4633

업데이트 5/28 :

  • 단기 솔루션으로서 ScalaCL 플러그인 (알파)은 간단한 스칼라 루프를 동등한 루프로 변환합니다 while.
  • 잠재적 인 장기 솔루션으로서 EPFL과 Stanford의 팀 은 “가상”스칼라 의 런타임 컴파일을 통해 고성능을 달성 할 수 있는 프로젝트에 협력하고 있습니다. 예를 들어, 여러 관용적 기능 루프 를 런타임시 최적의 JVM 바이트 코드 또는 GPU와 같은 다른 대상에 통합 할 수 있습니다 . 이 시스템은 확장 가능하므로 사용자 정의 DSL 및 변환이 가능합니다. 간행물 및 스탠포드 코스 노트를 확인하십시오 . 예비 코드는 Github에서 사용할 수 있으며 향후 몇 달 내에 릴리스 될 예정입니다.

답변

후속 조치로 -optimize 플래그를 시도하여 실행 시간을 103 초에서 76 초로 줄 였지만 Java 또는 while 루프보다 여전히 107 배 느립니다.

그런 다음 “기능”버전을보고있었습니다.

object P005 extends App{
  def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

간결한 방식으로 “전망”을 제거하는 방법을 알아 내려고 노력했습니다. 비참하게 실패했고

object P005_V2 extends App {
  def isDivis(x:Int):Boolean = {
    var i = 1
    while(i <= 20) {
      if (x % i != 0) return false
      i += 1
    }
    return true
  }
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

교활한 5 라인 솔루션이 12 라인으로 줄었습니다. 그러나이 버전은 원래 Java 버전과 동일한 속도로 0.71 초 동안 실행되며 “forall”(40.2s)을 사용하는 위 버전보다 56 배 더 빠릅니다! (이것이 Java보다 빠른 이유는 아래 편집을 참조하십시오)

분명히 다음 단계는 위의 내용을 Java로 다시 변환하는 것이었지만 Java는 처리 할 수 ​​없으며 22000 마크 주위에 n을 가진 StackOverflowError를 발생시킵니다.

그런 다음 머리를 조금 긁으면 서 “while”을 약간 더 많은 꼬리 재귀로 대체하여 몇 줄을 저장하고 빨리 실행하지만 얼굴을 마주 보았습니다.

object P005_V3 extends App {
  def isDivis(x:Int, i:Int):Boolean =
    if(i > 20) true
    else if(x % i != 0) false
    else isDivis(x, i+1)

  def find(n:Int):Int = if (isDivis(n, 2)) n else find (n+2)
  println (find (2))
}

스칼라의 꼬리 재귀가 하루에이기는 것이지만 “for”루프 (및 “forall”방법)와 같은 단순한 것이 본질적으로 깨져서 우아하고 장황한 “whiles”또는 꼬리 재귀로 대체되어야한다는 사실에 놀랐습니다. . 스칼라를 시도하는 많은 이유는 간결한 구문 때문이지만 코드가 100 배 느리게 실행되면 좋지 않습니다!

편집 : (삭제)

편집 편집 : 실행 시간 2.5와 0.7 사이의 불일치는 전적으로 32 비트 또는 64 비트 JVM 중 어느 것이 사용 되었는가에 기인합니다. 명령 행의 스칼라는 JAVA_HOME에서 설정 한 것을 사용하지만 Java는 사용 가능한 경우 64 비트를 사용합니다. IDE에는 자체 설정이 있습니다. 여기에 일부 측정 : Eclipse에서 스칼라 실행 시간


답변

이해력에 대한 대답은 옳지 만 전체 이야기는 아닙니다. returnin 의 사용은 isEvenlyDivisible무료가 아닙니다. 내부에 return을 사용 for하면 스칼라 컴파일러가 로컬이 아닌 리턴을 생성하도록합니다 (즉, 함수 외부로 리턴).

루프를 종료하기 위해 예외를 사용하여 수행됩니다. 예를 들어, 자신 만의 컨트롤 추상화를 구축 한 경우에도 마찬가지입니다.

def loop[T](times: Int, default: T)(body: ()=>T) : T = {
    var count = 0
    var result: T = default
    while(count < times) {
        result = body()
        count += 1
    }
    result
}

def foo() : Int= {
    loop(5, 0) {
        println("Hi")
        return 5
    }
}

foo()

“Hi”가 한 번만 인쇄됩니다.

returnin foo이탈 foo(예상치)에 유의하십시오 . 괄호 표현은 당신의 서명으로 볼 수있는 기능 문자이기 때문에 loop1, 인이 아닌 지역 수익을 생성하기 위해 힘 컴파일러를 return종료에 힘을 foo그냥하지 body.

Java (예 : JVM)에서 이러한 동작을 구현하는 유일한 방법은 예외를 발생시키는 것입니다.

다시 돌아 가기 isEvenlyDivisible:

def isEvenlyDivisible(a:Int, b:Int):Boolean = {
  for (i <- 2 to b)
    if (a % i != 0) return false
  return true
}

if (a % i != 0) return false함수는 리턴 값이있는 함수 리터럴이므로 리턴 값에 도달 할 때마다 런타임에서 예외를 처리하고 잡아야하는데, 이로 인해 약간의 GC 오버 헤드가 발생합니다.


답변

forall내가 발견 한 방법을 가속화하는 몇 가지 방법 :

원본 : 41.3s

def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}

범위를 미리 인스턴스화하므로 매번 새 범위를 만들지 않습니다. 9.0 s

val r = (1 to 20)
def isDivis(x:Int) = r forall {x % _ == 0}

범위 대신 목록으로 변환 : 4.8 초

val rl = (1 to 20).toList
def isDivis(x:Int) = rl forall {x % _ == 0}

나는 다른 몇 가지 컬렉션을 시도했지만 List가 가장 빠릅니다 (범위와 고차 함수를 완전히 피하는 것보다 여전히 7 배 느립니다).

스칼라를 처음 사용하는 동안 컴파일러는 위와 같이 메서드의 Range 리터럴을 가장 바깥 쪽 범위의 Range 상수로 자동 대체하여 빠르고 중요한 성능 향상을 쉽게 구현할 수 있다고 생각합니다. 또는 Java의 Strings 리터럴처럼 인턴하십시오.


각주 : 배열은 Range와 거의 동일하지만 흥미롭게도 새로운 forall방법 (아래 표시)을 포밍하면 64 비트에서 24 %, 32 비트에서 8 % 빨라졌습니다. 요인 수를 20에서 15로 줄임으로써 계산 크기를 줄이면 차이가 사라 졌으므로 가비지 수집 효과 일 수 있습니다. 원인이 무엇이든간에 전체 부하 상태에서 장시간 작동 할 때 중요합니다.

List에 대한 유사한 포주도 약 10 % 향상된 성능을 제공했습니다.

  val ra = (1 to 20).toArray
  def isDivis(x:Int) = ra forall2 {x % _ == 0}

  case class PimpedSeq[A](s: IndexedSeq[A]) {
    def forall2 (p: A => Boolean): Boolean = {
      var i = 0
      while (i < s.length) {
        if (!p(s(i))) return false
        i += 1
      }
      true
    }
  }
  implicit def arrayToPimpedSeq[A](in: Array[A]): PimpedSeq[A] = PimpedSeq(in)  


답변

스칼라에 대한 믿음을 잃을 수도있는 사람들에게 이런 종류의 문제가 거의 모든 기능적 언어의 성능에서 나올 수 있다고 언급하고 싶었습니다. Haskell에서 접기를 최적화하는 경우 종종 재귀 꼬리 호출 최적화 루프로 다시 작성해야합니다. 그렇지 않으면 성능 및 메모리 문제가 발생합니다.

FP가 아직 이런 것들에 대해 생각할 필요가없는 지점에 아직 최적화되어 있지 않다는 것은 불행한 일입니다. 그러나 이것은 전혀 스칼라의 문제가 아닙니다.


답변

스칼라와 관련된 문제는 이미 논의되었지만 주요 문제는 무차별 대입 알고리즘을 사용하는 것이 그리 시원하지 않다는 것입니다. 이것을 고려하십시오 (원래 Java 코드보다 훨씬 빠름).

def gcd(a: Int, b: Int): Int = {
    if (a == 0)
        b
    else
        gcd(b % a, a)
}
print (1 to 20 reduce ((a, b) => {
  a / gcd(a, b) * b
}))