Scalaでfor内包表記とループを最適化する方法は?


131

したがって、ScalaはJavaと同じくらい高速であるはずです。私が最初にJavaで取り組んだScalaのProject Eulerの問題をいくつか取り上げます。具体的には問題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))
}

Windows 7 64ビットでScala 2.9.0.1を使用します。パフォーマンスを向上させるにはどうすればよいですか?私は何か間違ったことをしていますか?それともJavaの方がはるかに高速ですか?


2
scalaシェルを使用してコンパイルまたは解釈しますか?
AhmetB-Google、2011年

これを行うには、試行分割(ヒント)を使用するよりも良い方法があります。
ハンマー2011年

2
これをどのように計時しているのかは示しません。runメソッドのタイミングを調整してみましたか?
アーロンノヴストラップ、2011年

2
@hammar-うん、それはペンと紙の方法でやった:高から始まる各数値の素因数を書き留めてから、より高い数値に対してすでに持っている因数に線を引いて、(5 * 2 * 2)で終わる*(19)*(3 * 3)*(17)*(2 * 2)*()*(7)*(13)*()*(11)= 232792560
Luigi Plinge

2
+1これは、私がSOで数週間見てきた中で最も興味深い質問です(これには、かなり長い間見てきた中で最良の答えがあります)。
Mia Clarke、

回答:


111

この特定の場合の問題は、for式内から戻ることです。これは、次に、囲んでいるメソッドでキャッチされるNonLocalReturnExceptionのスローに変換されます。オプティマイザーはforeachを排除できますが、スロー/キャッチをまだ排除できません。そして投げ/キャッチは高価です。しかし、そのようなネストされた戻りはScalaプログラムではまれであるため、オプティマイザーはまだこのケースに対処していません。オプティマイザーを改善する作業が進行中であり、この問題をすぐに解決することが期待されます。


9
返品が例外になることはかなり重いです。きっとどこかで文書化されていると思いますが、それは理解できない隠された魔法の臭いを持っています。それが本当に唯一の方法ですか?
skrebbel 2011年

10
クロージャーの内部から戻る場合は、それが最良のオプションのようです。もちろん、外部のクロージャーからの戻りは、バイトコードの戻り命令に直接変換されます。
Martin Odersky

1
私は何かを見落としていると確信していますが、代わりにクロージャー内からの戻り値をコンパイルして、囲まれたブールフラグと戻り値を設定し、クロージャー呼び出しが戻った後に確認してください。
ルークヒュッテマン

9
なぜ彼の関数アルゴリズムはまだ55倍遅いのですか?それはそのような恐ろしいパフォーマンスに悩まされるべきではないように見えます
エリヤ

4
今、2014年にもう一度テストしましたが、パフォーマンスは次のようになりました。java-> 0.3s; scala-> 3.6秒。scala最適化-> 3.5秒。Scala機能-> 4秒; 3年前よりもはるかに良く見えますが、それでも違いは大きすぎます。さらにパフォーマンスの向上を期待できますか?言い換えれば、マーティンは、理論上、可能な最適化のために残されたものはありますか?
sasha.sochka 14

80

問題は、おそらくforメソッド内の理解の使用ですisEvenlyDivisiblefor同等のwhileループで置き換えると、Javaとのパフォーマンスの違いが解消されます。

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

-optimizeScalaバージョン2.9でこのフラグを試してみてください。観測されたパフォーマンスは、使用中の特定のJVMと、ホットスポットを識別して最適化するための十分な「ウォームアップ」時間を持つJITオプティマイザに依存する場合があります。

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

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

アップデート5/28

  • 短期的な解決策として、ScalaCLプラグイン(アルファ版)は単純なScalaループを同等のwhileループに変換します。
  • 長期的なソリューションの可能性として、EPFLとスタンフォードのチームは、非常に高いパフォーマンスを実現する「仮想」Scalaのランタイムコンパイルを可能にするプロジェクトで協力しています。たとえば、複数の慣用的な機能ループを実行時に最適なJVMバイトコード、またはGPUなどの別のターゲットに融合できます。システムは拡張可能であり、ユーザー定義のDSLと変換が可能です。出版物とスタンフォードコースのノートをチェックしてください。暫定コードはGithubで利用可能で、今後数か月以内にリリースされる予定です。

6
すばらしい、for内包表記をwhileループに置き換えたところ、Javaバージョンとまったく同じ速度(+/- <1%)で実行されました。ありがとう... 1分間、Scalaへの信頼を失っていました。今すぐ良い関数アルゴリズムに
取り掛かるだけ

24
末尾再帰関数もwhileループと同じくらい高速であることは注目に値します(両方が非常に類似または同一のバイトコードに変換されるため)。
Rex Kerr、

7
これも私を一度迎えました。信じられないほどのスローダウンのため、アルゴリズムをコレクション関数の使用からネストされたwhileループ(レベル6!)に変換する必要がありました。これは、かなりターゲットを絞る必要があるものです。まともな(高速ではない)パフォーマンスが必要なときにそれを使用できない場合、優れたプログラミングスタイルは何ですか?
Raphael、

7
forそれではいつが適していますか?
OscarRyz 2011年

@OscarRyz-Scalaのforは、ほとんどの場合、Javaのfor(:)として動作します。
Mike Axiak

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.2秒)より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))
}

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

編集:(削除済み)

EDIT OF EDIT:2.5秒と0.7秒の実行時間の以前の不一致は、32ビットと64ビットのどちらのJVMが使用されていたかによるものです。コマンドラインからのScalaはJAVA_HOMEで設定されたものを使用しますが、Javaは64ビットを使用します(使用可能な場合)。IDEには独自の設定があります。ここでいくつかの測定:EclipseでのScala実行時間


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は常に値を返す式であることに注意してください。ここではreturnキーワードは必要ありません。
キリツク'28年

3
あなたの最後のバージョンは(P005_V3)より短く、より宣言と私見明確に書面で行うことができます:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Blaisorblade

@Blaisorbladeいいえ。これは、末尾再帰性を壊します。これは、バイトコードのwhileループに変換する必要があるため、実行が高速になります。
gzm0 2013年

4
私はあなたの要点を理解していますが、私の例は&&と||からまだ末尾再帰です @tailrecを使用して確認されたように、短絡評価を使用する:gist.github.com/Blaisorblade/5672562
Blaisorblade

8

理解のための答えは正しいですが、それだけではありません。returnin の使用はisEvenlyDivisible無料ではないことに注意してください。内でreturnを使用するとfor、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回だけ印刷します。

returnin fooが終了することに注意してくださいfoo(これは予想されることです)。括弧で囲まれた式は関数リテラルであるため、これをシグネチャで確認すると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のかなりの原因となる例外をキャッチする必要があり、リターンを持つ関数リテラルです。


6

forall私が発見した方法をスピードアップするいくつかの方法:

オリジナル:41.3秒

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

範囲を事前にインスタンス化するため、毎回新しい範囲を作成しません:9.0秒

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が最速でした(ただし、Rangeと高次関数を完全に回避した場合よりも7倍遅くなりました)。

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


脚注:配列は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

私は、このような問題がほとんどすべての関数型言語のパフォーマンスに現れるというScalaへの信頼を失う可能性のある人々にコメントしたいと思います。Haskellでフォールドを最適化している場合、それを再帰的な末尾呼び出し最適化ループとして書き直す必要があることがよくあります。そうしないと、パフォーマンスとメモリの問題に対処する必要があります。

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


2

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

質問では、特定のロジックのパフォーマンスを言語間で比較します。アルゴリズムが問題に最適かどうかは重要ではありません。
smartnut007 2014

1

プロジェクトオイラーのScalaソリューションで提供されているワンライナーを試してください

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


それは私の機能バージョンとかなり似ています。あなたは私のようdef r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2)に書くことができます、これはPavelのものより4文字短いです。:)しかし、私は自分のコードが良いとは思わない-この質問を投稿したとき、合計で約30行のScalaをコーディングした。
Luigi Plinge
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.