Das Beispiel
entspricht dem folgenden Java-Code:
Code: Alles auswählen
static Obj example() {
Obj result = new List();
Obj iter = new Range(new Int(1000000)).iter();
Obj k = iter.next();
while (k != null) {
result.append(k.mul(k));
k = iter.next();
}
return result;
}
Natürlich müssen wir noch schnell eine Python-artige Laufzeitumgebung hinzufügen und `Obj`, `Int`, `Range` und `List` implementieren. `Obj` ist die abstrakte Oberklasse für alle Datentypen und stellt, damit ich nicht laufend casten muss, alle benutzten Methoden als Stub zur Verfügung:
Code: Alles auswählen
static abstract class Obj {
public Obj next() { throw new UnsupportedOperationException(); }
public void append(Obj obj) { throw new UnsupportedOperationException(); }
public Obj mul(Obj other) { throw new UnsupportedOperationException(); }
}
`List` ist ein Wrapper um `java.util.ArrayList<E>`:
Code: Alles auswählen
static class List extends Obj {
private final ArrayList<Obj> values = new ArrayList<Obj>();
public void append(Obj obj) {
values.add(obj);
}
}
Meine IDE warnt mich an dieser Stelle, dass ich nie auf `values` zugreife. Das ist ein gutes Indiz dafür, dass auch die Java-VM das erkennen könnte und gnadenlos meinen Benchmark wegoptimieren könnte. Wir werden sehen...
Weiter mit `Int`, die im Gegensatz zu Python nur 32-Bit breit sind. Das führt zu falschen Ergebnissen, aber das korrigieren wir später:
Code: Alles auswählen
static class Int extends Obj {
private final int value;
public Int(int value) {
this.value = value;
}
public Obj mul(Obj other) {
return new Int(value * ((Int) other).value);
}
}
Hier ist `Range`, wo ich für den Iterator eine anonyme innere Klasse benutze:
Code: Alles auswählen
static class Range extends Obj {
private final int stop;
public Range(Obj stop) {
this.stop = ((Int) stop).value;
}
public Obj iter() {
return new Obj() {
private int index;
public Obj next() {
return index < stop ? new Int(index++) : null;
}
};
}
}
Lasse ich das Beispiel jetzt wie folgt unter Java 6 laufen, sehe ich eine interessante Zeitverteilung: Es beginnt bei ca. 200ms, geht dann zweimal auf 400ms hoch um letztlich bei 150ms zu landen. Ich tippe darauf, das die längeren Laufzeiten daran liegen, dass hier die JVM den Code kompiliert. Mit Java 7 gehe ich letztlich auf etwa 30ms runter, braucht zwischendurch aber auch mal 300ms. Möglicherweise sind die längeren Durchläufe auch dem GC geschuldet.
Code: Alles auswählen
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
long t = System.currentTimeMillis();
example();
System.out.println(System.currentTimeMillis() - t);
}
}
Diese Zeitverteilung ist typisch für JIT-Systeme und macht das Benchmarking so schwer. Ein Durchlauf ist viel zu wenig. Die Laufzeiten sind außerdem viel zu gering. Speziell Java ist darauf getrimmt, langlaufende Prozesse zu optimieren, nicht Code, der wenige Millisekunden lang läuft. Dort macht der JIT in der Regel nicht die Mühe. Wahrscheinlich misst dieser Benchmark die Leistung des GC, denn ich erzeuge immerhin 20.000.001 `Int`-Objekte und diverse `Obj[]` während die `ArrayList`-Objekte wachsen.
Da ich eh eine 64-bit-VM habe, kann ich ohne Performance-Nachteil von `int` auf `long` wechseln, damit mein Programm auch korrekt rechnet.
Was mich allerdings misstrauisch macht ist, dass es keinen Unterschied macht, ob ich die `ArrayList` wachsen lasse oder gleich auf 1.000.000 Elemente initialisiere. Ich hätte gedacht, man würde hier einen Unterschied wahrnehmen können. Wird das Array doch wegoptimiert? Lasse ich mir mal das letzte Element des Array zurückgeben, steigt die typische Laufzeit unter Java 7 auf 40ms. Initialisiere ich jetzt das Array auf 1.000.000 vor, habe ich wieder die alte Laufzeit. Erwischt! Da hat sich also die JVM das speichern meiner `Int` in dem Array und das Array selbst gesparrt, weil ich's nie gebraucht habe.
Ein weiterer Punkt, warum in der Regel gilt, dass Micro-Benchmarks lügen.
Stefan