Anscheinend ist auf meinem Windows 8-Laptop mit HotSpot JDK 1.7.0_45 (wobei alle Compiler- / VM-Optionen auf Standard gesetzt sind) die folgende Schleife
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}
ist mindestens 2 Größenordnungen schneller (~ 10 ms gegenüber ~ 5000 ms) als:
final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {
}
Ich habe dieses Problem beim Schreiben einer Schleife bemerkt, um ein anderes irrelevantes Leistungsproblem zu bewerten. Und der Unterschied zwischen ++i < n
und i++ < n
war groß genug, um das Ergebnis signifikant zu beeinflussen.
Wenn wir uns den Bytecode ansehen, lautet der Schleifenkörper der schnelleren Version:
iinc
iload
ldc
if_icmplt
Und für die langsamere Version:
iload
iinc
ldc
if_icmplt
Daher ++i < n
erhöht es zuerst die lokale Variable i
um 1 und schiebt sie dann auf den Operandenstapel, während i++ < n
diese beiden Schritte in umgekehrter Reihenfolge ausgeführt werden. Das scheint aber nicht zu erklären, warum Ersteres viel schneller ist. Gibt es im letzteren Fall eine temporäre Kopie? Oder sollte etwas jenseits des Bytecodes (VM-Implementierung, Hardware usw.) für den Leistungsunterschied verantwortlich sein?
Ich habe eine andere Diskussion las über ++i
und i++
(nicht erschöpfend obwohl), aber fand keine Antwort , die Java-spezifisch ist und direkt auf den Fall bezogen , wo ++i
oder i++
in einem Wertvergleich beteiligt ist.
Antworten:
Wie andere betont haben, ist der Test in vielerlei Hinsicht fehlerhaft.
Sie haben uns nicht genau gesagt, wie Sie diesen Test durchgeführt haben. Ich habe jedoch versucht, einen "naiven" Test (keine Beleidigung) wie folgt durchzuführen:
class PrePostIncrement { public static void main(String args[]) { for (int j=0; j<3; j++) { for (int i=0; i<5; i++) { long before = System.nanoTime(); runPreIncrement(); long after = System.nanoTime(); System.out.println("pre : "+(after-before)/1e6); } for (int i=0; i<5; i++) { long before = System.nanoTime(); runPostIncrement(); long after = System.nanoTime(); System.out.println("post : "+(after-before)/1e6); } } } private static void runPreIncrement() { final int n = Integer.MAX_VALUE; int i = 0; while (++i < n) {} } private static void runPostIncrement() { final int n = Integer.MAX_VALUE; int i = 0; while (i++ < n) {} } }
Wenn Sie dies mit Standardeinstellungen ausführen, scheint es einen kleinen Unterschied zu geben. Der wahre Fehler des Benchmarks wird jedoch offensichtlich, wenn Sie dies mit der
-server
Flagge ausführen . Die Ergebnisse in meinem Fall sind dann ungefähr so... pre : 6.96E-4 pre : 6.96E-4 pre : 0.001044 pre : 3.48E-4 pre : 3.48E-4 post : 1279.734543 post : 1295.989086 post : 1284.654267 post : 1282.349093 post : 1275.204583
Offensichtlich wurde die Pre-Inkrement-Version komplett weg optimiert . Der Grund ist ziemlich einfach: Das Ergebnis wird nicht verwendet. Es spielt überhaupt keine Rolle, ob die Schleife ausgeführt wird oder nicht, daher entfernt die JIT sie einfach.
Dies wird durch einen Blick auf die Hotspot-Demontage bestätigt: Die Pre-Inkrement-Version führt zu folgendem Code:
[Entry Point] [Verified Entry Point] [Constants] # {method} {0x0000000055060500} 'runPreIncrement' '()V' in 'PrePostIncrement' # [sp+0x20] (sp of caller) 0x000000000286fd80: sub $0x18,%rsp 0x000000000286fd87: mov %rbp,0x10(%rsp) ;*synchronization entry ; - PrePostIncrement::runPreIncrement@-1 (line 28) 0x000000000286fd8c: add $0x10,%rsp 0x000000000286fd90: pop %rbp 0x000000000286fd91: test %eax,-0x243fd97(%rip) # 0x0000000000430000 ; {poll_return} 0x000000000286fd97: retq 0x000000000286fd98: hlt 0x000000000286fd99: hlt 0x000000000286fd9a: hlt 0x000000000286fd9b: hlt 0x000000000286fd9c: hlt 0x000000000286fd9d: hlt 0x000000000286fd9e: hlt 0x000000000286fd9f: hlt
Die Post-Inkrement-Version führt zu folgendem Code:
[Entry Point] [Verified Entry Point] [Constants] # {method} {0x00000000550605b8} 'runPostIncrement' '()V' in 'PrePostIncrement' # [sp+0x20] (sp of caller) 0x000000000286d0c0: sub $0x18,%rsp 0x000000000286d0c7: mov %rbp,0x10(%rsp) ;*synchronization entry ; - PrePostIncrement::runPostIncrement@-1 (line 35) 0x000000000286d0cc: mov $0x1,%r11d 0x000000000286d0d2: jmp 0x000000000286d0e3 0x000000000286d0d4: nopl 0x0(%rax,%rax,1) 0x000000000286d0dc: data32 data32 xchg %ax,%ax 0x000000000286d0e0: inc %r11d ; OopMap{off=35} ;*goto ; - PrePostIncrement::runPostIncrement@11 (line 36) 0x000000000286d0e3: test %eax,-0x243d0e9(%rip) # 0x0000000000430000 ;*goto ; - PrePostIncrement::runPostIncrement@11 (line 36) ; {poll} 0x000000000286d0e9: cmp $0x7fffffff,%r11d 0x000000000286d0f0: jl 0x000000000286d0e0 ;*if_icmpge ; - PrePostIncrement::runPostIncrement@8 (line 36) 0x000000000286d0f2: add $0x10,%rsp 0x000000000286d0f6: pop %rbp 0x000000000286d0f7: test %eax,-0x243d0fd(%rip) # 0x0000000000430000 ; {poll_return} 0x000000000286d0fd: retq 0x000000000286d0fe: hlt 0x000000000286d0ff: hlt
Mir ist nicht ganz klar, warum die Post-Inkrement-Version anscheinend nicht entfernt wird. (Tatsächlich betrachte ich dies als separate Frage). Aber zumindest erklärt dies, warum Sie möglicherweise Unterschiede mit einer "Größenordnung" sehen ...
EDIT: Interessant ist , wenn die obere Grenze der Schleife ändert ,
Integer.MAX_VALUE
umInteger.MAX_VALUE-1
dann beide sind Versionen wegoptimiert und erfordern „Null“ der Zeit. Irgendwie verhindert diese Grenze (die immer noch wie0x7fffffff
in der Baugruppe erscheint) die Optimierung. Vermutlich hat dies etwas damit zu tun, dass der Vergleich einer (versengten!)cmp
Anweisung zugeordnet wird, aber darüber hinaus kann ich keinen tiefgreifenden Grund nennen. Die JIT arbeitet auf mysteriöse Weise ...quelle
while (i++ < Integer.MAX_VALUE)
Verlassen der Schleife ist bereits ein Überlauf passierti
. Der Nachweis der Richtigkeit einer Codetransformation ist viel schwieriger, wenn ein Überlauf auftreten kann, und schließlich sind Schleifen mit Überläufen nicht der übliche Fall. Warum sollte sich der Hotspot also die Mühe machen, sie zu optimierenDer Unterschied zwischen ++ i und i ++ besteht darin, dass ++ i die Variable effektiv inkrementiert und diesen neuen Wert "zurückgibt". i ++ hingegen erstellt effektiv eine temporäre Variable, die den aktuellen Wert in i enthält, und erhöht dann die Variable, die den Wert der temporären Variablen zurückgibt. Hier kommt der zusätzliche Overhead her.
// i++ evaluates to something like this // Imagine though that somehow i was passed by reference int temp = i; i = i + 1; return temp; // ++i evaluates to i = i + 1; return i;
In Ihrem Fall scheint das Inkrement von der JVM nicht optimiert zu werden, da Sie das Ergebnis in einem Ausdruck verwenden. Die JVM kann andererseits eine solche Schleife optimieren.
for( int i = 0; i < Integer.MAX_VALUE; i++ ) {}
Dies liegt daran, dass das Ergebnis von i ++ niemals verwendet wird. In einer solchen Schleife sollten Sie in der Lage sein, sowohl ++ i als auch i ++ mit der gleichen Leistung zu verwenden, als ob Sie ++ i verwendet hätten.
quelle
++i
Version möglich, für die andere nicht?BEARBEITEN 2
Sie sollten hier wirklich schauen:
http://hg.openjdk.java.net/code-tools/jmh/file/f90aef7f1d2c/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_11_Loops.java
BEARBEITEN Je mehr ich darüber nachdenke, desto mehr wird mir klar, dass dieser Test irgendwie falsch ist. Die Schleife wird von der JVM ernsthaft optimiert.
Ich denke, dass Sie das einfach fallen
@Param
lassen und lassen solltenn=2
.Auf diese Weise testen Sie die Leistung des
while
selbst. Die Ergebnisse bekomme ich in diesem Fall:o.m.t.WhileTest.testFirst avgt 5 0.787 0.086 ns/op o.m.t.WhileTest.testSecond avgt 5 0.782 0.087 ns/op
Das ist fast kein Unterschied
Die allererste Frage, die Sie sich stellen sollten, ist, wie Sie dies testen und messen . Dies ist Mikro-Benchmarking und in Java ist dies eine Kunst, und fast immer wird ein einfacher Benutzer (wie ich) die Ergebnisse falsch verstehen. Sie sollten sich auf einen Benchmark-Test und ein sehr gutes Werkzeug verlassen. Ich habe JMH verwendet, um dies zu testen:
@Measurement(iterations=5, time=1, timeUnit=TimeUnit.MILLISECONDS) @Fork(1) @Warmup(iterations=5, time=1, timeUnit=TimeUnit.SECONDS) @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) @State(Scope.Benchmark) public class WhileTest { public static void main(String[] args) throws Exception { Options opt = new OptionsBuilder() .include(".*" + WhileTest.class.getSimpleName() + ".*") .threads(1) .build(); new Runner(opt).run(); } @Param({"100", "10000", "100000", "1000000"}) private int n; /* @State(Scope.Benchmark) public static class HOLDER_I { int x; } */ @Benchmark public int testFirst(){ int i = 0; while (++i < n) { } return i; } @Benchmark public int testSecond(){ int i = 0; while (i++ < n) { } return i; } }
Jemand, der viel mehr Erfahrung mit JMH hat, könnte diese Ergebnisse korrigieren (ich hoffe es wirklich!, Da ich in JMH noch nicht so vielseitig bin), aber die Ergebnisse zeigen, dass der Unterschied verdammt gering ist:
Benchmark (n) Mode Samples Score Score error Units o.m.t.WhileTest.testFirst 100 avgt 5 1.271 0.096 ns/op o.m.t.WhileTest.testFirst 10000 avgt 5 1.319 0.125 ns/op o.m.t.WhileTest.testFirst 100000 avgt 5 1.327 0.241 ns/op o.m.t.WhileTest.testFirst 1000000 avgt 5 1.311 0.136 ns/op o.m.t.WhileTest.testSecond 100 avgt 5 1.450 0.525 ns/op o.m.t.WhileTest.testSecond 10000 avgt 5 1.563 0.479 ns/op o.m.t.WhileTest.testSecond 100000 avgt 5 1.418 0.428 ns/op o.m.t.WhileTest.testSecond 1000000 avgt 5 1.344 0.120 ns/op
Das Feld Score ist das Feld, an dem Sie interessiert sind.
quelle
Wahrscheinlich reicht dieser Test nicht aus, um Schlussfolgerungen zu ziehen, aber ich würde sagen, wenn dies der Fall ist, kann die JVM diesen Ausdruck optimieren, indem sie i ++ in ++ i ändert, da der gespeicherte Wert von i ++ (Vorwert) in dieser Schleife niemals verwendet wird.
quelle
Ich schlage vor, Sie sollten (wann immer möglich) immer verwenden,
++c
anstatt,c++
da erstere niemals langsamer werden, dac
im letzteren Fall konzeptionell eine tiefe Kopie von erstellt werden muss, um den vorherigen Wert zurückzugeben.In der Tat optimieren viele Optimierer eine unnötige tiefe Kopie, aber sie können dies nicht einfach tun, wenn Sie den Ausdruckswert verwenden. Und genau das tun Sie in Ihrem Fall.
Viele Leute sind sich jedoch nicht einig: Sie sehen darin eine Mikrooptimierung.
quelle
if (CONSTANT == var)
undif (CONSTANT.equals(var))