Scala soll also so schnell sein wie Java. Ich besuche einige Project Euler- Probleme in Scala, die ich ursprünglich in Java angegangen bin. Speziell Problem 5: "Was ist die kleinste positive Zahl, die durch alle Zahlen von 1 bis 20 gleichmäßig teilbar ist?"
Hier ist meine Java-Lösung, die auf meinem Computer 0,7 Sekunden dauert:
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();
}
}
Hier ist meine "direkte Übersetzung" in Scala, die 103 Sekunden dauert (147 Mal länger!)
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
}
}
Zum Schluss noch mein Versuch einer funktionalen Programmierung, die 39 Sekunden dauert (55-mal länger).
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))
}
Verwenden von Scala 2.9.0.1 unter Windows 7 64-Bit. Wie verbessere ich die Leistung? Mache ich etwas falsch? Oder ist Java nur viel schneller?
java
performance
scala
for-loop
while-loop
Luigi Plinge
quelle
quelle
run
Methode zeitlich zu steuern?Antworten:
Das Problem in diesem speziellen Fall ist, dass Sie aus dem for-Ausdruck zurückkehren. Dies wird wiederum in einen Wurf einer NonLocalReturnException übersetzt, die bei der einschließenden Methode abgefangen wird. Der Optimierer kann den Foreach eliminieren, aber den Wurf / Fang noch nicht eliminieren. Und werfen / fangen ist teuer. Da solche verschachtelten Rückgaben in Scala-Programmen jedoch selten sind, hat der Optimierer diesen Fall noch nicht behandelt. Es wird daran gearbeitet, den Optimierer zu verbessern, der dieses Problem hoffentlich bald lösen wird.
quelle
Das Problem ist höchstwahrscheinlich die Verwendung eines
for
Verständnisses in der MethodeisEvenlyDivisible
. Das Ersetzenfor
durch eine äquivalentewhile
Schleife sollte den Leistungsunterschied zu Java beseitigen.Im Gegensatz zu Javas
for
Schleifen ist Scalasfor
Verständnis tatsächlich syntaktischer Zucker für Methoden höherer Ordnung. In diesem Fall rufen Sie dieforeach
Methode für einRange
Objekt auf. Scalafor
ist sehr allgemein, führt aber manchmal zu schmerzhaften Leistungen.Vielleicht möchten Sie das
-optimize
Flag in Scala Version 2.9 ausprobieren . Die beobachtete Leistung kann von der jeweils verwendeten JVM abhängen, und der JIT-Optimierer verfügt über eine ausreichende Aufwärmzeit, um Hotspots zu identifizieren und zu optimieren.Jüngste Diskussionen auf der Mailingliste zeigen, dass das Scala-Team
for
in einfachen Fällen daran arbeitet, die Leistung zu verbessern :Hier ist das Problem im Bug-Tracker: https://issues.scala-lang.org/browse/SI-4633
Update 5/28 :
while
Schleifen um.quelle
for
dann geeignet?Als Folge habe ich das Flag -optimize ausprobiert und es hat die Laufzeit von 103 auf 76 Sekunden reduziert, aber das ist immer noch 107x langsamer als Java oder eine while-Schleife.
Dann habe ich mir die "funktionale" Version angesehen:
und versuchen herauszufinden, wie man das "Forall" auf prägnante Weise loswird. Ich habe kläglich versagt und mir etwas ausgedacht
wobei meine listige 5-Zeilen-Lösung auf 12 Zeilen aufgestockt wurde. Diese Version läuft jedoch in 0,71 Sekunden , der gleichen Geschwindigkeit wie die ursprüngliche Java-Version und 56-mal schneller als die obige Version mit "forall" (40,2 s)! (siehe EDIT unten, warum dies schneller als Java ist)
Natürlich bestand mein nächster Schritt darin, das oben Gesagte wieder in Java zu übersetzen, aber Java kann damit nicht umgehen und wirft einen StackOverflowError mit n um die 22000-Marke.
Ich kratzte mir dann ein bisschen am Kopf und ersetzte das "while" durch ein bisschen mehr Schwanzrekursion, die ein paar Zeilen spart, genauso schnell läuft, aber seien wir ehrlich, es ist verwirrender zu lesen:
Scalas Schwanzrekursion gewinnt also den Tag, aber ich bin überrascht, dass etwas so Einfaches wie eine "for" -Schleife (und die "forall" -Methode) im Wesentlichen kaputt ist und durch unelegante und wortreiche "whiles" oder Schwanzrekursion ersetzt werden muss . Viele der Gründe, warum ich Scala ausprobiere, sind die präzise Syntax, aber es ist nicht gut, wenn mein Code 100-mal langsamer ausgeführt wird!
BEARBEITEN : (gelöscht)
EDIT OF EDIT : Frühere Abweichungen zwischen Laufzeiten von 2,5 s und 0,7 s waren ausschließlich darauf zurückzuführen, ob 32-Bit- oder 64-Bit-JVMs verwendet wurden. Scala über die Befehlszeile verwendet alles, was von JAVA_HOME festgelegt wurde, während Java 64-Bit verwendet, sofern verfügbar, unabhängig davon. IDEs haben ihre eigenen Einstellungen. Einige Messungen hier: Scala-Ausführungszeiten in Eclipse
quelle
def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)
. Beachten Sie, dass in Scala if-else ein Ausdruck ist, der immer einen Wert zurückgibt. Hier wird das Schlüsselwort return nicht benötigt.P005_V3
) kann kürzer,def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Die Antwort zum Verständnis ist richtig, aber es ist nicht die ganze Geschichte. Sie sollten beachten, dass die Verwendung von
return
inisEvenlyDivisible
nicht kostenlos ist. Die Verwendung von return innerhalb vonfor
zwingt den Scala-Compiler, eine nicht lokale Rückgabe zu generieren (dh außerhalb seiner Funktion zurückzukehren).Dies erfolgt durch die Verwendung einer Ausnahme zum Verlassen der Schleife. Das gleiche passiert, wenn Sie Ihre eigenen Steuerelementabstraktionen erstellen, zum Beispiel:
Dies druckt "Hi" nur einmal.
Beachten Sie, dass die
return
In-foo
Exitsfoo
(was Sie erwarten würden). Da der Ausdruck in Klammern ein Funktionsliteral ist, das Sie in der Signaturloop
dieses Ausdrucks sehen können, wird der Compiler gezwungen, eine nicht lokale Rückgabe zu generieren,return
dh die Sie zum Beenden zwingenfoo
, nicht nur diebody
.In Java (dh der JVM) besteht die einzige Möglichkeit, ein solches Verhalten zu implementieren, darin, eine Ausnahme auszulösen.
Zurück zu
isEvenlyDivisible
:Das
if (a % i != 0) return false
ist ein Funktionsliteral, das eine Rückgabe hat. Jedes Mal, wenn die Rückgabe getroffen wird, muss die Laufzeit eine Ausnahme auslösen und abfangen, was einen erheblichen GC-Overhead verursacht.quelle
Einige Möglichkeiten, um die von
forall
mir entdeckte Methode zu beschleunigen :Das Original: 41,3 s
Vorinstanziieren des Bereichs, damit nicht jedes Mal ein neuer Bereich erstellt wird: 9,0 s
Konvertieren in eine Liste anstelle eines Bereichs: 4,8 s
Ich habe ein paar andere Sammlungen ausprobiert, aber List war am schnellsten (obwohl immer noch 7x langsamer als wenn wir die Range- und die Funktion höherer Ordnung insgesamt vermeiden).
Obwohl ich neu in Scala bin, würde ich vermuten, dass der Compiler leicht einen schnellen und signifikanten Leistungsgewinn erzielen kann, indem er Range-Literale in Methoden (wie oben) automatisch durch Range-Konstanten im äußersten Bereich ersetzt. Oder besser, internieren Sie sie wie Strings-Literale in Java.
Fußnote : Arrays waren ungefähr gleich wie Range, aber interessanterweise führte das Pimpen einer neuen
forall
Methode (siehe unten) zu einer 24% schnelleren Ausführung auf 64-Bit und 8% schneller auf 32-Bit. Als ich die Berechnungsgröße durch Reduzieren der Anzahl der Faktoren von 20 auf 15 reduzierte, verschwand der Unterschied, sodass es sich möglicherweise um einen Speicherbereinigungseffekt handelt. Was auch immer die Ursache sein mag, es ist wichtig, wenn Sie längere Zeit unter Volllast arbeiten.Ein ähnlicher Zuhälter für List führte auch zu einer um etwa 10% besseren Leistung.
quelle
Ich wollte nur für Leute, die wegen solcher Probleme das Vertrauen in Scala verlieren könnten, kommentieren, dass diese Art von Problemen bei der Ausführung nahezu aller funktionalen Sprachen auftreten. Wenn Sie eine Falte in Haskell optimieren, müssen Sie sie häufig als rekursive, für Endanrufe optimierte Schleife neu schreiben, da sonst Leistungs- und Speicherprobleme auftreten.
Ich weiß, dass es bedauerlich ist, dass FPs noch nicht so weit optimiert sind, dass wir nicht über solche Dinge nachdenken müssen, aber dies ist überhaupt kein spezielles Problem für Scala.
quelle
Scala-spezifische Probleme wurden bereits diskutiert, aber das Hauptproblem ist, dass die Verwendung eines Brute-Force-Algorithmus nicht sehr cool ist. Beachten Sie Folgendes (viel schneller als der ursprüngliche Java-Code):
quelle
Probieren Sie den in der Lösung Scala for Project Euler angegebenen Einzeiler aus
Die angegebene Zeit ist mindestens schneller als Ihre, obwohl weit entfernt von der while-Schleife .. :)
quelle
def r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2)
, was 4 Zeichen kürzer als die von Pavel ist. :) Allerdings tue ich nicht so, als wäre mein Code gut - als ich diese Frage stellte, hatte ich insgesamt etwa 30 Zeilen Scala codiert.