Performance vs. Java

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
da.dom
User
Beiträge: 114
Registriert: Dienstag 10. Juni 2008, 14:42

Hallo Zusammen,

eigentlich bin ich Java Programmierer, schreibe meine Quick&Dirty Scripte aber gerne mit Python.
Nun bin ich auf ein mathematisches Rätsel gestoßen, das 9 unbekannte Variablen in 6 Gleichungen einander in Beziehung stellt.
Die Gleichungen sehen zum Beispiel so aus: ABC+BDE=FGH, ADB-ECB=IJ .....

Ohne mir über Optimierung Gedanken zu machen habe ich dazu ein Skript geschrieben, dass das Rätsel innherhalb von 1,5Stunden lösen konnte (hat auch über 3 Milliarden durchläufe gebraucht). Da ich wie gesagt Java Entwickler bin habe ich dieses Mini-Programm mal schnell in Java Portiert (man kann ja doch nicht ganz aus seine Haut), um einen Performance vergleich zu machen, mit der Erwartung das Python wesentlich schneller arbeitet.

Überraschender Weise hat sich ein extremes Gegenteil eingestellt.
Pyhton benötigt: 1,5h
Java: unter 1min

Es geht mir weniger um die Optimierung des Programmes, sondern viel mehr um die Frage warum Python (das ich immer für schneller gehalten habe) in diesme Fall so viel langsamer ist als Java, oder bin ich einem Irrglauben unterlegen das der Geschwindigkeits-Vorteil generell bei Python liegt?

Hier die beiden Programme, die genau dasselbe tun:

Code: Alles auswählen

from datetime import datetime

print "*Start*",  datetime.now()

import sys
run=0

for a in range(0,10):
  print "Analyzing a=", a
  for b in range(0,10):
    for c in range(0,10):
      for d in range(0,10):
        for e in range(0,10):
          for f in range (0,10):
            for g in range(0,10):
              for h in range(0,10):
                for i in range(0,10):
                  for j in range(0,10):
                    run=run+1
                    #zeile 1
                    if ((a*100+b*10+c)+(b*100+d*10+e))==f*100+g*10+h:
                      #zeile 2
                      if ((a*100+d*10+b)-(e*100+c*10+b))==i*10+j:
                        #zeile3
                        if((g*100+c*10+e)+(e*100+a*10+c))==h*100+j*10+h:
                          #spalte1
                          if ((a*100+b*10+c)+(a*100+d*10+b))==g*100+c*10+e:
                            #spalte2
                            if ((b*100+d*10+e)-(e*100+c*10+b))==e*100+a*10+c:     
                              #spalte3
                              if ((f*100+g*10+h)+(i*10+j))==h*100+j*10+h:
                                print "Matching "
                                print  "a=", a
                                print  "b=", b
                                print  "c=", c
                                print  "d=", d
                                print  "e=", e
                                print  "f=", f
                                print  "g=", g
                                print  "h=", h
                                print  "i=", i
                                print  "j=", j
                                print
                                print datetime.now()
                                print "Checks ", run
                                print "*******"
Java:

Code: Alles auswählen

		System.out.println("*Start* " + format.format(new Date()));

		int run = 0;

		for (int a = 0; a < 10; a++) {
			System.out.println("Analyzing a=" + a);
			for (int b = 0; b < 10; b++) {
				for (int c = 0; c < 10; c++) {
					for (int d = 0; d < 10; d++) {
						for (int e = 0; e < 10; e++) {
							for (int f = 0; f < 10; f++) {
								for (int g = 0; g < 10; g++) {
									for (int h = 0; h < 10; h++) {
										for (int i = 0; i < 10; i++) {
											for (int j = 0; j < 10; j++) {
												run++;
												// #zeile 1
												if ((a * 100 + b * 10 + c) + (b * 100 + d * 10 + e) == f * 100 + g * 10 + h) {
													// #zeile 2
													if ((a * 100 + d * 10 + b) - (e * 100 + c * 10 + b) == i * 10 + j) {
														// #zeile3
														if ((g * 100 + c * 10 + e) + (e * 100 + a * 10 + c) == h * 100 + j * 10 + h) {
															// #spalte1
															if ((a * 100 + b * 10 + c) + (a * 100 + d * 10 + b) == g * 100 + c * 10 + e) {
																// #spalte2
																if ((b * 100 + d * 10 + e) - (e * 100 + c * 10 + b) == e * 100 + a * 10 + c) {
																	// #spalte3
																	if ((f * 100 + g * 10 + h) + (i * 10 + j) == h * 100 + j * 10 + h) {
																		System.out.println("Matching ");
																		System.out.println("a=" + a);
																		System.out.println("b=" + b);
																		System.out.println("c=" + c);
																		System.out.println("d=" + d);
																		System.out.println("e=" + e);
																		System.out.println("f=" + f);
																		System.out.println("g=" + g);
																		System.out.println("h=" + h);
																		System.out.println("i=" + i);
																		System.out.println("j=" + j);
																		System.out.println();
																		System.out.println("*Start* " + format.format(new Date()));
																		System.out.println("Checks " + run);
																		System.out.println("*******");

																	}
																}
															}
														}
													}
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}

	}
Viele Grüße
Dom
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

da.dom hat geschrieben:oder bin ich einem Irrglauben unterlegen das der Geschwindigkeits-Vorteil generell bei Python liegt?
Irrglaube. Java ist im Allgemeinen *deutlich* schneller als Python. Das liegt zum einen an der Java-Hotspot-VM die sehr gut optimiert(die Python-VM optimiert so gut wie nichts), zum anderen an Java selbst, das Optimierungen durch das primitivere Objektmodell sehr viel einfacher macht.
BlackJack

@da.dom: Ja, Du bist wohl einem Irrglauben unterlegen wenn Du glaubst, dass der Geschwindigkeitsvorteil generell bei Python liegt. Wie bist Du denn darauf gekommen? Ich gehe jetzt mal von CPython aus: Eine Interpreter für Bytecode mit dynamischer Typisierung bei dem alles was man an einen Namen binden kann ein Objekt ist. Dagegen Java: statisch typisiert und in der Regel wird der Bytecode durch einen JIT-Compiler zu Maschinencode übersetzt, der direkt vom Prozessor ausgeführt wird.

Ausserdem könnte ich mir vorstellen, dass bei dem Python-Code eine Menge Zeit für das anlegen und zerstören der ganzen Listen drauf geht, die bei den ganzen `range()`-Aufrufen entstehen. Da hätte man *ein* Objekt immer wiederverwenden können.

Edit: Und man sollte so etwas immer in eine Funktion stecken. Nicht nur weil es sauberer ist, sondern weil CPython lokale Namen schneller auflöst als (modul)globale.
BlackJack

@da.dom: Du hast wahrscheinlich einen schnelleren Rechner als ich. Ich habe es mal laufen lassen und von a=0 bis a=1 braucht mein Rechner ≈20 Minuten. Hoch gerechnet also 20m·10 = 3h20m. Wenn ich das in eine Funktion packe, ist es hoch gerechnet schon mal eine Stunde weniger.

Ich schrob in meinem letzten Beitrag absichtlich immer CPython statt nur Python, denn die Implementierung macht auch einen Unterschied. Mit PyPy hat es komplett durchgelaufen „nur“ ≈14 Minuten gedauert.

Das Programm macht übrigens 10 Milliarden Durchläufe und nicht nur 3.

Das Java-Programm braucht bei mir ≈1½ Minuten. Ich sagte ja mein Rechner ist langsamer. Eine interessante Ausgabe beim Java-Programm: 'Checks -723680355'. So ein Java-``int`` kann halt nicht bis 10 Milliarden zählen. :-)

Das heisst allerdings nicht, dass man kein Python-Programm schreiben kann, dass schneller ist, als Dein Java-Programm.

So viel muss man dazu gar nicht selber schreiben wenn man zum Beispiel das `logilab.constraint`-Paket verwendet. Damit beschreibt man das Problem in dem man die Variablen, ihre Wertebereiche, und Einschränkungen angibt. Die Lösung sucht dann das `Solver`-Objekt aus dem Paket. Ich habe noch ein wenig Quelltext dazu geschrieben um die Eingabe der Gleichungen einfacher zu machen:

Code: Alles auswählen

import re
from itertools import imap
from logilab.constraint import fd, Repository, Solver

SOURCE = """\
abc + bde = fgh
adb - ecb = ij
gce + eac = hjh
abc + adb = gce
bde - ecb = eac
fgh + ij = hjh
"""


def make_number(names):
    result = ['%s * %s' % (n, 10**i) for i, n in enumerate(reversed(names))]
    result[0] = names[-1]
    return ' + '.join(reversed(result))


class Equation(object):
    
    parse_line = re.compile(r'\w+|[+-]').findall
    
    def __init__(self, operand_a, operation, operand_b, result):
        self.operand_a = operand_a
        self.operation = operation
        self.operand_b = operand_b
        self.result = result
    
    def __str__(self):
        return '(%s) %s (%s) == %s' % (
            make_number(self.operand_a),
            self.operation,
            make_number(self.operand_b),
            make_number(self.result)
        )
    
    @property
    def variables(self):
        return set(self.operand_a + self.operand_b + self.result)
    
    @classmethod
    def from_string(cls, line):
        return cls(*cls.parse_line(line))


def parse_equations(lines):
    variables = set()
    equations = list()
    for equation in imap(Equation.from_string, lines):
        variables.update(equation.variables)
        equations.append(equation)
    return sorted(variables), equations


def main():
    variables, equations = parse_equations(SOURCE.splitlines())
    print 'Variablen:', ' '.join(variables)
    print 'Gleichungen:'
    for equation in equations:
        print ' ', equation
    values = xrange(10)
    domains = dict((v, fd.FiniteDomain(values)) for v in variables)
    constraints = [
        fd.make_expression(tuple(e.variables), str(e)) for e in equations
    ]
    repository = Repository(variables, domains, constraints)
    solver = Solver()
    solutions = solver.solve_all(repository)
    print 'Loesungen:'
    for solution in solutions:
        print ' ', ' '.join('%s=%s' % item for item in sorted(solution.items()))


if __name__ == '__main__':
    main()
Das läuft bei mir in 22 Sekunden (!) durch und liefert folgende Ausgabe:

Code: Alles auswählen

Variablen: a b c d e f g h i j
Gleichungen:
  (a * 100 + b * 10 + c) + (b * 100 + d * 10 + e) == f * 100 + g * 10 + h
  (a * 100 + d * 10 + b) - (e * 100 + c * 10 + b) == i * 10 + j
  (g * 100 + c * 10 + e) + (e * 100 + a * 10 + c) == h * 100 + j * 10 + h
  (a * 100 + b * 10 + c) + (a * 100 + d * 10 + b) == g * 100 + c * 10 + e
  (b * 100 + d * 10 + e) - (e * 100 + c * 10 + b) == e * 100 + a * 10 + c
  (f * 100 + g * 10 + h) + (i * 10 + j) == h * 100 + j * 10 + h
Loesungen:
  a=0 b=0 c=0 d=0 e=0 f=0 g=0 h=0 i=0 j=0
  a=3 b=5 c=7 d=1 e=2 f=8 g=6 h=9 i=4 j=0
Da ist ja nun immer noch das falsche Ergebnis dabei weil nicht berücksichtigt wird, dass die erste Ziffer von jeder "Zahl" keine 0 sein darf. Wenn man bei diesen Variablen den Wertebereich von 0 bis 9 auf 1 bis 9 einschränkt…

Code: Alles auswählen

# ...
Equation(object):
    # ...
    @property
    def leading_variables(self):
        return set(self.operand_a[0] + self.operand_b[0] + self.result[0])
    # ...

# ...

def parse_equations(lines):
    variables = set()
    leading_variables = set()
    equations = list()
    for equation in imap(Equation.from_string, lines):
        variables.update(equation.variables)
        leading_variables.update(equation.leading_variables)
        equations.append(equation)
    return sorted(variables), leading_variables, equations

# ...

def main():
    variables, leading_variables, equations = parse_equations(
        SOURCE.splitlines()
    )
    print 'Variablen:', ' '.join(variables)
    print 'Gleichungen:'
    for equation in equations:
        print ' ', equation
    domains = dict(
        (v, fd.FiniteDomain(xrange(1 if v in leading_variables else 0, 10)))
        for v in variables
    )
…dann läuft das Ganze in unter 2 Sekunden! Dabei ist `logilab.constraint` reines Python ohne C-Erweiterung.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Spannend ist natürlich auch die Betrachtung, wie schnell man von der Fragestellung zur Lösung kommt. Das Brute-Force-Programm ist in 10min in den Computer gehackt und inklusive Laufzeit hat man dann in etwa 3h das Ergebnis.

Wahrscheinlich hätte man sich auch noch den schnellsten Rechner bei Amazon EC2 nehmen können, dort das Programm laufen lassen können und sagen wir einfach mal, es braucht da 1,5h und man braucht etwa 10min, um sich dort zu registrieren, eine Instanz anzufordern und laufen zu lassen, hat man's unter 2h für $1-$2 plus 20min eigene Zeit.

Kennt man das Constraint-Resolver Paket nicht und muss sich noch ein ein relativ komplexes Programmkonzept einarbeiten und dann doch ein recht langes Programm schreiben, welches vielleicht nicht das erste Mal korrekt ist, bin ich mir nicht sicher, dass man das ganze in 2h schafft.

Somit war die erste Lösung (selbst in Python) vielleicht die schnellste.

Das Programm lässt sich übrigens nahezu perfekt parallelisieren. Einfach die äußerste For-schleife parallelisieren sollte nicht mehr als weitere 5min kosten (leider ist das nicht so trivial in CPython, man muss das multiprocessing-Modul benutzen), und das sollte die Laufzeit auf einem üblichen Rechner um Faktor 4 reduzieren, auf einem Amazon-Boliden schafft man vielleicht sogar Faktor 10 und landet so bei etwa 20min, immer noch mit dem trivialen Brute-Force-Ansatz.

Oder man nimmt eben eine Programmiersprache, die für diesen Anwendungsfall schneller ist.

Java rechnet mit ints (oder longs, denn "run" muss ein long sein) viel, viel, viel, viel schneller als CPython. Und auch im allgemeinen Fall ist Java wie schon erwähnt deutlich schneller als Python (CPython oder andere Implementierungen). Ich wage mich mal mit dieser Verallgemeinerung in einen Flamewar:

Richtig langsam: PHP, Ruby 1.8
Langsam: Python, Ruby 1.9, Squeak
Schneller: JavaScript, Lua
Schnell: Java, C#, C++

Jede Stufe entspricht ungefähr einer Größenordnung.

Für einige Fälle kommt Google's V8-Implementierung von JavaScript an Java heran und für andere Fälle ist C oder C++ doch noch schneller als Java oder C#. Lua ist wahrscheinlich der schnellste Interpreter und Dart (nicht aufgeführt) will mal schneller sein, als V8. Andere JVM-basierte Sprachen liegen mehr oder weniger stark unter Java. Clojure und Scala lägen gleichauf, JRuby ist eine und Jython zwei Stufen tiefer. Rhino irgendwo dazwischen.

Stefan
BlackJack

@sma: Ich denke für Python würde ich schon die 14 Minuten veranschlagen, die PyPy für das Original benötigt. Denn feststellen, dass das Original mit CPython (zu) lange dauert und es stattdessen mit PyPy laufen zu lassen, sollte insgesamt weniger Zeit in Anspruch nehmen als CPython wirklich komplett durchlaufen zu lassen.

Ansonsten könnte man beim Original noch `psyco` versuchen, sofern für die Plattform vorhanden. Oder `itertools.product()` statt der vielen Schleifen. Wäre selbst wenn es gleich langsam bliebe, eine Überlegung Wert um die Einrücktiefe zu verringern.
deets

@BlackJack

Angesichts der Tatsache dass Psyco + PyPy aus der Feder derselben Genies stammen - Armin Rigo & Christian Tismer- bezweifele ich, dass psyco da einen echten Gewinn einfaehrt.

Ich finde PyPy gerade unglaublich spannend. Christian hat mir ein Video gezeigt von Armin, bei dem der einen Canny-Edge Filter auf einen Webcam-Stream in Echtzeit anwendet. CPython braucht dafuer ~1-2 min / frame. Sehr beeindruckend.

Und auch all die anderen Optionen fuer PyPy sind fabelhaft - GIL-removal, verschiedene Backends... ich bin wirklich sehr enthusiasmiert was das angeht.
BlackJack

@deets: `psyco` wahr eher als präventive Verteidigung gedacht, falls das gleiche Argument wie bei `logilab.constraint` kommt, dass man PyPy halt erst einmal kennen muss. `psyco` gibt es schon recht lange und wird im Netz oft als eine der Antworten gegeben wie man (C)Python bei solchen Programmen etwas Beine machen kann. Das findet man sicher deutlich schneller im Web als einen FDC-Solver für Python, wo man ja auch erst einmal die Suchbegriffe kennen muss um da überhaupt drauf zu stossen.
Benutzeravatar
gkuhl
User
Beiträge: 600
Registriert: Dienstag 25. November 2008, 18:03
Wohnort: Hong Kong

Wenn man Cython nutzt und nur die Integer `a` bis `j` und `run` (als 'unsigned long') deklariert geht es in wenigen Sekunden.
Antworten