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


131

스칼라는 자바만큼 빠르다. 스칼라에서 원래 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가 훨씬 더 빠릅니까?


2
scala shell을 사용하여 컴파일하거나 해석합니까?
AhmetB-Google

평가판 나누기 ( 힌트 )를 사용하는 것보다이 방법을 사용하는 것이 좋습니다 .
hammar

2
당신은 이것을 어떻게 타이밍하는지 보여주지 않습니다. run방법의 타이밍을 시도 했습니까 ?
Aaron Novstrup

2
@hammar-네, 방금 펜과 종이 방식을 사용했습니다. 높은 숫자로 시작하는 각 숫자의 주요 요인을 기록한 다음 더 높은 숫자에 대해 이미 가지고있는 요인을 제거하여 (5 * 2 * 2) * (19) * (3 * 3) * (17) * (2 * 2) * () * (7) * (13) * () * (11) = 232792560
Luigi Plinge

2
+1 이것은 몇 주 동안 제가 SO에서 본 가장 흥미로운 질문입니다.
Mia Clarke

답변:


111

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


9
반품이 예외가 될 정도로 무겁습니다. 나는 그것이 어딘가에 문서화되어 있다고 확신하지만, 이해할 수없는 숨겨진 마술의 악취가 있습니다. 그게 유일한 방법인가요?
skrebbel

10
클로저 내부에서 반품이 발생하면 가장 적합한 옵션 인 것 같습니다. 외부 클로저의 리턴은 물론 바이트 코드의 명령을 리턴하도록 직접 변환됩니다.
Martin Odersky 2016 년

1
나는 무언가를 간과하고 있다고 확신하지만 대신 닫힌 부울 플래그와 반환 값을 설정하기 위해 클로저 내부에서 리턴을 컴파일하고 클로저 호출이 반환 된 후 확인하십시오.
Luke Hutteman 2016 년

9
그의 기능 알고리즘이 여전히 55 배 더 느린 이유는 무엇입니까? 그런 끔찍한 공연으로 고통받는 것 같지 않습니다
Elijah

4
이제 2014 년에 이것을 다시 테스트했으며 성능은 다음과 같습니다. java-> 0.3s; 스칼라-> 3.6s; 스칼라 최적화-> 3.5s; 스칼라 기능성-> 4s; 3 년 전보다 훨씬 좋아 보이지만 ... 여전히 차이가 너무 큽니다. 더 많은 성능 향상을 기대할 수 있습니까? 다시 말해, Martin, 이론적으로 가능한 최적화를 위해 남겨둔 것이 있습니까?
sasha.sochka

80

문제는 아마도 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에서 사용할 수 있으며 향후 몇 달 내에 릴리스 될 예정입니다.

6
글쎄, 나는 이해력을 while 루프로 바꾸었고 Java 버전과 정확히 같은 속도 (+/- <1 %)를 실행한다. 고마워 ... 나는 스칼라에 대한 믿음을 거의 잃어 버렸다. 이제는 좋은 기능 알고리즘을 연구해야합니다 ... :)
Luigi Plinge

24
tail-recursive 함수는 while 루프만큼 빠릅니다 (둘 다 매우 유사하거나 동일한 바이트 코드로 변환되므로).
렉스 커

7
이것은 나도 한 번 얻었다. 놀라운 속도 저하로 인해 컬렉션 함수 사용에서 중첩 된 while 루프 (레벨 6!)로 알고리즘을 변환해야했습니다. 이것은 엄청나게 목표가되어야하는 것입니다. 괜찮은 성능을 필요로 할 때 사용할 수 없다면 좋은 프로그래밍 스타일은 무엇입니까?
Raphael

7
for그렇다면 언제 적절한가요?
OscarRyz

@OscarRyz-scala의 for는 대부분 Java의 for (:)와 같이 작동합니다.
Mike Axiak 12

31

후속 조치로 -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에서 스칼라 실행 시간


1
isDivis-method는 다음과 같이 쓸 수 있습니다 def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1). Scala if-else는 항상 값을 반환하는 표현식입니다. 여기서는 키워드를 반환 할 필요가 없습니다.
kiritsuku

3
마지막 버전 ( P005_V3)은 다음과 같이 작성하여 더 짧고 선언적이며 IMHO 명확하게 만들 수 있습니다.def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Blaisorblade

@Blaisorblade No. 이것은 꼬리 재귀를 깨뜨릴 것이며 바이트 코드의 while 루프로 변환하는 데 필요합니다.
gzm0

4
요점을 알지만 && 및 || 이후로 여전히 예제가 꼬리 재귀입니다. : @tailrec 사용하여 확인 된, 단락 회로 평가를 사용 gist.github.com/Blaisorblade/5672562
Blaisorblade을

8

이해력에 대한 대답은 옳지 만 전체 이야기는 아닙니다. 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 오버 헤드가 발생합니다.


6

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)  

3

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

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


2

스칼라와 관련된 문제는 이미 논의되었지만 주요 문제는 무차별 대입 알고리즘을 사용하는 것이 그리 시원하지 않다는 것입니다. 이것을 고려하십시오 (원래 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
}))

질문은 언어 간 특정 논리의 성능을 비교합니다. 알고리즘이 문제에 최적인지 여부는 중요하지 않습니다.
smartnut007

1

프로젝트 오일러 솔루션 스칼라에 제공된 하나의 라이너를 사용해보십시오.

while 루프와는 거리가 멀지 만 주어진 시간은 적어도 당신보다 빠릅니다 .. :)


내 기능 버전과 매우 비슷합니다. 내 것으로 쓸 수 있습니다 def r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2). 이는 Pavel보다 4 자 더 짧습니다. :) 그러나 나는 내 코드가 좋은 척하지 않습니다.이 질문을 게시했을 때 총 약 30 줄의 스칼라를 코딩했습니다.
Luigi Plinge
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.