web-dev-qa-db-ja.com

Scalaでfor理解とループを最適化する方法は?

だからScalaはJavaと同じくらい速いはずです。私はいくつかの Project Euler の問題を再検討していますScala 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();
    }
}

これがScalaへの「直接翻訳」で、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))
}

Scala 2.9.0.1 Windows 7 64ビットでの使用。パフォーマンスを改善するにはどうすればよいですか?何か間違ったことをしていますか?またはJavaずっと速いですか?

この特定の場合の問題は、for-expression内から戻ることです。これは、NonLocalReturnExceptionのスローに変換され、外側のメソッドでキャッチされます。オプティマイザーはforeachを削除できますが、スロー/キャッチをまだ削除できません。また、スロー/キャッチは高価です。ただし、Scalaプログラムではこのようなネストされた戻り値はまれであるため、オプティマイザーはこのケースにまだ対処していません。

111
Martin Odersky

問題は、おそらくメソッドforisEvenlyDivisible内包表記を使用していることです。 forを同等のwhileループに置き換えると、Javaとのパフォーマンスの違いがなくなるはずです。

Javaのforループとは対照的に、Scalaのfor内包表記は、実際には高階メソッドの構文糖衣です。この場合、foreachオブジェクトのRangeメソッドを呼び出しています。 Scalaのforは非常に一般的ですが、パフォーマンスに苦痛をもたらすこともあります。

-optimize flag in Scala version 2.9。観察されるパフォーマンスは、使用中の特定のJVM、およびホットスポットを特定して最適化するのに十分な「ウォームアップ」時間を持つJITオプティマイザーによって異なります。

メーリングリストに関する最近の議論は、Scalaチームが単純なケースでforパフォーマンスの改善に取り組んでいることを示しています。

バグトラッカーの問題は次のとおりです。 https://issues.scala-lang.org/browse/SI-46

5/28を更新

  • 短期的な解決策として、 ScalaCL プラグイン(アルファ)は、単純なScalaループをwhileループに相当するものに変換します。
  • 潜在的な長期ソリューションとして、EPFLとスタンフォードのチームは プロジェクトでの共同作業 非常に高いパフォーマンスのために "virtual" Scala のランタイムコンパイルを有効にします。たとえば、複数の慣用的な機能ループを 実行時に融合 から最適なJVMバイトコードに、またはGPUなどの別のターゲットにできます。このシステムは拡張可能で、ユーザー定義のDSLと変換が可能です。 publications およびStanford course notes を確認してください。予備コードはGithubで入手でき、今後数か月でリリースされる予定です。
80
Kipton Barros

フォローアップとして、-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))
}

そして、「forall」を簡潔に取り除く方法を見つけようとしています。惨めに失敗して思いついた

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

これにより、私のcな5行ソリューションは12行に膨れ上がりました。ただし、このバージョンは0.71秒で実行され、元のJavaバージョンと同じ速度で、56倍高速です「forall」(40.2 s)を使用した上記バージョン!(これがJavaより速い理由については、下記のEDITを参照)

明らかに、次のステップは上記を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))
}

そのため、Scalaの末尾再帰が勝ちますが、「for」ループ(および「forall」メソッド)のような単純なものが本質的に壊れており、エレガントで冗長な「whiles」、または末尾再帰に置き換える必要があることに驚いています。私がしようとしている多くの理由Scalaは構文が簡潔であるためですが、コードが100倍遅く実行されるのは良くありません!

[〜#〜] edit [〜#〜]:(削除済み)

EDIT OF EDIT:実行時間2.5秒と0.7秒の間の以前の矛盾は、32ビットまたは64ビットJVMのどちらが使用されていたかに完全に起因していました。 ScalaはコマンドラインからJava_HOMEで設定されたものを使用し、Javaは使用可能であれば64ビットを使用します。IDEには独自の設定があります。 Eclipseでのスカラ実行時間

理解のための答えは正しいですが、それは全体の話ではありません。 returnでのisEvenlyDivisibleの使用は無料ではないことに注意してください。 forの内部でreturnを使用すると、scalaコンパイラーが非ローカル戻り値を生成するように強制します(つまり、関数の外部に戻ります)。

これは、ループを終了する例外を使用して行われます。独自の制御抽象化を構築する場合も同じことが起こります。たとえば、次のとおりです。

_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」を1回だけ印刷します。

returnfoofooを終了することに注意してください(これは予想どおりです)。括弧で囲まれた式は関数リテラルであるため、loopのシグネチャで確認できます。これにより、コンパイラは強制的に非ローカルリターンを生成します。つまり、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オーバーヘッドが発生します。

8
juancn

私が発見したforallメソッドを高速化するいくつかの方法:

オリジナル:41.3 s

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 s

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

他のいくつかのコレクションを試しましたが、リストは最速でした(ただし、範囲と高次関数を完全に回避する場合よりも7倍遅い)。

私はScalaを初めて使用しますが、メソッドのRangeリテラルを(上記のように)自動的に最も外側のスコープのRange定数に置き換えるだけで、コンパイラは迅速かつ大幅なパフォーマンス向上を簡単に実装できると思います。または、Javaの文字列リテラルのようにインターンしてください。


footnote:配列はRangeとほぼ同じでしたが、興味深いことに、新しいforallメソッド(下記参照)をポンピングすると、64ビットでの実行が24%高速になり、8%で高速になりました32ビット。ファクターの数を20から15に減らして計算サイズを小さくすると、差はなくなりました。したがって、ガベージコレクションの効果である可能性があります。原因が何であれ、長時間にわたって全負荷で動作する場合は重要です。

リストの同様のポン引きも、パフォーマンスが約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)  

このような問題についてScalaを信頼しているかもしれない人々に対してコメントしたいだけです。 Haskellは、多くの場合、再帰的な末尾呼び出しに最適化されたループとして書き直す必要があります。そうしないと、パフォーマンスとメモリの問題に対処する必要があります。

FPがこのようなことを考える必要がないほど最適化されていないのは残念ですが、これはScalaに特有の問題ではありません。

3
Ara Vartanian

Scalaに固有の問題はすでに議論されていますが、主な問題は、ブルートフォースアルゴリズムの使用はあまりクールではないことです。これを考慮してください(元の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
}))
2
Sarge Borsch

ソリューションで指定されたワンライナーを試してくださいプロジェクトオイラーのスカラ

指定された時間は、whileループからは程遠いものの、少なくともあなたのものより速いです。

1
eivindw