Ich bin neu in Java und habe letzte Nacht Code ausgeführt, und das hat mich wirklich gestört. Ich habe ein einfaches Programm erstellt, um alle X-Ausgänge in einer for-Schleife anzuzeigen, und ich habe einen massiven Leistungsabfall festgestellt, als ich den Modul als variable % variable
vs variable % 5000
oder so weiter verwendet habe. Kann mir jemand erklären, warum das so ist und was es verursacht? Also kann ich besser sein ...
Hier ist der "effiziente" Code (Entschuldigung, wenn ich ein bisschen falsche Syntax habe, bin ich gerade nicht mit dem Code auf dem Computer)
long startNum = 0;
long stopNum = 1000000000L;
for (long i = startNum; i <= stopNum; i++){
if (i % 50000 == 0) {
System.out.println(i);
}
}
Hier ist der "ineffiziente Code"
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++){
if (i % progressCheck == 0) {
System.out.println(i);
}
}
Wohlgemerkt, ich hatte eine Datumsvariable, um die Unterschiede zu messen, und sobald sie lang genug war, dauerte die erste 50 ms, während die andere 12 Sekunden oder so etwas dauerte. Möglicherweise müssen Sie die erhöhen stopNum
oder verringern, progressCheck
wenn Ihr PC effizienter als meiner ist oder was nicht.
Ich habe im Internet nach dieser Frage gesucht, aber ich kann keine Antwort finden. Vielleicht stelle ich sie einfach nicht richtig.
EDIT: Ich hatte nicht erwartet, dass meine Frage so beliebt ist, ich schätze alle Antworten. Ich habe für jede Hälfte der Zeit einen Benchmark durchgeführt, und der ineffiziente Code dauerte erheblich länger, 1/4 Sekunde gegenüber 10 Sekunden Geben oder Nehmen. Zugegeben, sie verwenden println, aber beide machen die gleiche Menge, daher würde ich mir nicht vorstellen, dass dies zu einer starken Verzerrung führen würde, zumal die Diskrepanz wiederholbar ist. Da ich neu in Java bin, werde ich die Stimmen vorerst entscheiden lassen, welche Antwort die beste ist. Ich werde versuchen, bis Mittwoch einen auszuwählen.
EDIT2: Ich werde heute Abend einen weiteren Test durchführen, bei dem anstelle des Moduls nur eine Variable inkrementiert wird. Wenn progressCheck erreicht ist, wird eine durchgeführt und diese Variable für eine dritte Option auf 0 zurückgesetzt.
EDIT3.5:
Ich habe diesen Code verwendet und unten zeige ich meine Ergebnisse. Vielen Dank an ALLE für die wunderbare Hilfe! Ich habe auch versucht, den kurzen Wert des langen mit dem Wert 0 zu vergleichen, sodass alle meine neuen Überprüfungen immer "65536" Mal durchgeführt werden, sodass sie in Wiederholungen gleich sind.
public class Main {
public static void main(String[] args) {
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 65536;
final long finalProgressCheck = 50000;
long date;
// using a fixed value
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if (i % 65536 == 0) {
System.out.println(i);
}
}
long final1 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
//using a variable
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
System.out.println(i);
}
}
long final2 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using a final declared variable
for (long i = startNum; i <= stopNum; i++) {
if (i % finalProgressCheck == 0) {
System.out.println(i);
}
}
long final3 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using increments to determine progressCheck
int increment = 0;
for (long i = startNum; i <= stopNum; i++) {
if (increment == 65536) {
System.out.println(i);
increment = 0;
}
increment++;
}
//using a short conversion
long final4 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if ((short)i == 0) {
System.out.println(i);
}
}
long final5 = System.currentTimeMillis() - date;
System.out.println(
"\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
}
}
Ergebnisse:
- fest = 874 ms (normalerweise um 1000 ms, aber schneller, da es eine Potenz von 2 ist)
- variabel = 8590 ms
- letzte Variable = 1944 ms (war ~ 1000 ms bei Verwendung von 50000)
- Inkrement = 1904 ms
- Kurzumwandlung = 679 ms
Kein Wunder, dass die Short Conversion aufgrund mangelnder Teilung 23% schneller war als der "schnelle" Weg. Das ist interessant zu bemerken. Wenn Sie alle 256 Mal (oder ungefähr dort) etwas zeigen oder vergleichen müssen, können Sie dies tun und verwenden
if ((byte)integer == 0) {'Perform progress check code here'}
EIN ENDGÜLTIGER INTERESSANTER HINWEIS: Die Verwendung des Moduls für die "endgültig deklarierte Variable" mit 65536 (keine hübsche Zahl) war halb so schnell (langsamer) als der feste Wert. Wo vorher war es Benchmarking in der Nähe der gleichen Geschwindigkeit.
quelle
final
vor derprogressCheck
Variablen hinzufüge , laufen beide wieder mit der gleichen Geschwindigkeit. Das lässt mich glauben, dass der Compiler oder die JIT es schaffen, die Schleife zu optimieren, wenn sie weiß, dass diesprogressCheck
konstant ist.Antworten:
Sie messen den OSR- Stub (On-Stack-Ersatz) .
OSR-Stub ist eine spezielle Version der kompilierten Methode, die speziell zum Übertragen der Ausführung vom interpretierten Modus in kompilierten Code während der Ausführung der Methode vorgesehen ist.
OSR-Stubs sind nicht so optimiert wie normale Methoden, da sie ein Frame-Layout benötigen, das mit dem interpretierten Frame kompatibel ist. Ich habe dies bereits in den folgenden Antworten gezeigt: 1 , 2 , 3 .
Ähnliches passiert auch hier. Während "ineffizienter Code" eine lange Schleife ausführt, wird die Methode speziell für den On-Stack-Austausch direkt innerhalb der Schleife kompiliert. Der Status wird vom interpretierten Frame an die OSR-kompilierte Methode übertragen, und dieser Status enthält eine
progressCheck
lokale Variable. Zu diesem Zeitpunkt kann JIT die Variable nicht durch die Konstante ersetzen und daher bestimmte Optimierungen wie die Festigkeitsreduzierung nicht anwenden .Insbesondere ist diese Mittel JIT ersetzen nicht ganzzahlige Division mit Multiplikation . (Siehe Warum verwendet GCC bei der Implementierung der Ganzzahldivision die Multiplikation mit einer seltsamen Zahl? Für den asm-Trick eines Compilers vor der Zeit, wenn der Wert nach Inlining / Konstanten-Propagation eine Konstante zur Kompilierungszeit ist, wenn diese Optimierungen aktiviert sind Ein ganzzahliges Literalrecht im
%
Ausdruck wird ebenfalls von optimiertgcc -O0
, ähnlich wie hier, wo es vom JITer selbst in einem OSR-Stub optimiert wird.)Wenn Sie dieselbe Methode jedoch mehrmals ausführen, führen der zweite und der nachfolgende Lauf den regulären Code (ohne OSR) aus, der vollständig optimiert ist. Hier ist ein Benchmark zum Nachweis der Theorie ( Benchmarking mit JMH ):
Und die Ergebnisse:
Die allererste Iteration von
divVar
ist in der Tat viel langsamer, da der OSR-Stub ineffizient kompiliert wurde. Sobald die Methode jedoch von Anfang an erneut ausgeführt wird, wird die neue, nicht eingeschränkte Version ausgeführt, die alle verfügbaren Compiler-Optimierungen nutzt.quelle
System.out.println
fast wird zwangsläufig Müll Ergebnisse produzieren, und die Tatsache , dass beide Versionen gleich schnell muss nicht mit OSR alles tun , insbesondere , soweit ich das sagen kann ..1
ist etwas zweifelhaft - die leere Schleife könnte auch komplett wegoptimiert werden. Der zweite ist diesem ähnlicher. Aber auch hier ist nicht klar, warum Sie den Unterschied speziell dem OSR zuschreiben . Ich würde nur sagen: Irgendwann ist die Methode JITed und wird schneller. Nach meinem Verständnis bewirkt das OSR nur, dass die Verwendung des endgültigen, optimierten Codes (ungefähr) "auf den nächsten Optimierungsdurchlauf verschoben" wird. (Fortsetzung ...)%
Operation das gleiche Gewicht, da eine optimierte Ausführung nur möglich ist, wenn ein Optimierer tatsächlich arbeitet. Die Tatsache, dass eine Schleifenvariante erheblich schneller als die andere ist, beweist das Vorhandensein eines Optimierers und beweist ferner, dass es nicht gelungen ist, eine der Schleifen im gleichen Maße wie die andere zu optimieren (innerhalb derselben Methode!). Da diese Antwort die Fähigkeit beweist, beide Schleifen im gleichen Maße zu optimieren, muss etwas vorhanden sein, das die Optimierung behindert. Und das ist OSR in 99,9% aller Fälle-XX:+PrintCompilation -XX:+TraceNMethodInstalls
.Im Anschluss an den @ phuclv- Kommentar habe ich den von JIT 1 generierten Code überprüft. Die Ergebnisse lauten wie folgt:
für
variable % 5000
(Division durch Konstante):für
variable % variable
:Da die Division immer länger dauert als die Multiplikation, ist das letzte Code-Snippet weniger performant.
Java-Version:
1 - Verwendete VM-Optionen:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main
quelle
imul
3 Zyklen,idiv
liegt zwischen 30 und 90 Zyklen. Die Ganzzahldivision ist also zwischen 10x und 30x langsamer als die Ganzzahlmultiplikation.Wie andere angemerkt haben, erfordert die allgemeine Moduloperation eine Division. In einigen Fällen kann die Division (durch den Compiler) durch eine Multiplikation ersetzt werden. Beide können jedoch im Vergleich zur Addition / Subtraktion langsam sein. Daher kann die beste Leistung von etwas in dieser Richtung erwartet werden:
(Als kleinen Optimierungsversuch verwenden wir hier einen Abwärtszähler vor der Dekrementierung, da bei vielen Architekturen im Vergleich zu
0
unmittelbar nach einer arithmetischen Operation genau 0 Befehle / CPU-Zyklen kosten, da die Flags der ALU durch die vorhergehende Operation bereits entsprechend gesetzt sind. Eine anständige Optimierung Der Compiler führt diese Optimierung jedoch automatisch durch, selbst wenn Sie schreibenif (counter++ == 50000) { ... counter = 0; }
.)Beachten Sie, dass Sie den Modul oft nicht wirklich wollen / brauchen, weil Sie wissen, dass Ihr Schleifenzähler (
i
) oder was auch immer immer nur um 1 erhöht wird, und Sie sich wirklich nicht um den tatsächlichen Rest kümmern, den der Modul Ihnen gibt, sehen Sie einfach wenn der um eins erhöhte Zähler einen Wert erreicht.Ein weiterer "Trick" besteht darin, Zweierpotenzwerte / -grenzen zu verwenden, z
progressCheck = 1024;
. Der Modul einer Zweierpotenz kann schnell bitweise berechnet werdenand
, dif ( (i & (1024-1)) == 0 ) {...}
. H. Dies sollte auch ziemlich schnell sein und kann bei einigen Architekturen diecounter
oben genannten Anforderungen übertreffen .quelle
if()
Körper wird zu einem Körper der äußeren Schleife, und das Material außerhalb des Körpers wird zu einem Körper derif()
inneren Schleife, der fürmin(progressCheck, stopNum-i)
Iterationen ausgeführt wird. Zu Beginn und jedes Malcounter
, wenn 0 erreicht ist, müssen Sielong next_stop = i + min(progressCheck, stopNum-i);
einefor(; i< next_stop; i++) {}
Schleife einrichten . In diesem Fall ist diese innere Schleife leer und sollte hoffentlich vollständig optimiert werden. Sie können dies in der Quelle tun und es dem JITer leicht machen, indem Sie Ihre Schleife auf i + = 50k reduzieren.--counter
ist genauso schnell wie meine Inkrement-Version, aber weniger Code. Außerdem war es 1 niedriger als es sein sollte. Ich bin gespannt, ob es sein solltecounter--
, um die genaue Nummer zu erhalten, die Sie wollen , nicht, dass es ein großer Unterschied ist--counter
ist um eins aus.counter--
gibt Ihnen genau dieprogressCheck
Anzahl der Iterationen (oder Sie könntenprogressCheck = 50001;
natürlich einstellen ).Ich bin auch überrascht, wie gut die oben genannten Codes funktionieren. Es geht um die Zeit, die der Compiler für die Ausführung des Programms gemäß der deklarierten Variablen benötigt. Im zweiten (ineffizienten) Beispiel:
Sie führen die Moduloperation zwischen zwei Variablen aus. Hier muss der Compiler jedes Mal nach jeder Iteration den Wert von überprüfen
stopNum
undprogressCheck
zu dem spezifischen Speicherblock gehen, der sich für diese Variablen befindet, da es sich um eine Variable handelt und sich ihr Wert möglicherweise ändert.Aus diesem Grund ging der Compiler nach jeder Iteration zum Speicherort, um den neuesten Wert der Variablen zu überprüfen. Daher konnte der Compiler zum Zeitpunkt der Kompilierung keinen effizienten Bytecode erstellen.
Im ersten Codebeispiel führen Sie einen Moduloperator zwischen einer Variablen und einem konstanten numerischen Wert aus, der sich innerhalb der Ausführung nicht ändert, und der Compiler muss den Wert dieses numerischen Werts nicht vom Speicherort aus überprüfen. Aus diesem Grund konnte der Compiler effizienten Bytecode erstellen. Wenn Sie erklären
progressCheck
als einefinal
oder alsfinal static
Variable dann zum Zeitpunkt der Laufzeit / Kompilierung-Compiler weiß , dass es eine letzte Variable ist und sein Wert wird nicht dann den Compiler ersetzen ändernprogressCheck
mit50000
in Code:Jetzt können Sie sehen, dass dieser Code auch wie das erste (effiziente) Codebeispiel aussieht. Die Leistung des ersten Codes und wie oben erwähnt, funktioniert effizient. Es wird keinen großen Unterschied in der Ausführungszeit eines der beiden Codebeispiele geben.
quelle
volatile
die ‚Compiler‘ wird nicht seinen Wert aus dem RAM immer und immer wieder lesen.