だから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プログラムではこのようなネストされた戻り値はまれであるため、オプティマイザーはこのケースにまだ対処していません。
問題は、おそらくメソッドfor
でisEvenlyDivisible
内包表記を使用していることです。 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を更新:
while
ループに相当するものに変換します。フォローアップとして、-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回だけ印刷します。
return
の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オーバーヘッドが発生します。
私が発見した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に特有の問題ではありません。
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
}))
ソリューションで指定されたワンライナーを試してくださいプロジェクトオイラーのスカラ
指定された時間は、whileループからは程遠いものの、少なくともあなたのものより速いです。