Wir arbeiten an einer latenzempfindlichen Anwendung und haben alle Arten von Methoden (mit jmh ) mikrobenchmarkiert . Nachdem ich eine Suchmethode mit einem Mikrobenchmarking versehen und mit den Ergebnissen zufrieden war, implementierte ich die endgültige Version und stellte fest, dass die endgültige Version dreimal langsamer war als das, was ich gerade bewertet hatte.
Der Schuldige war, dass die implementierte Methode ein enum
Objekt anstelle eines zurückgab int
. Hier ist eine vereinfachte Version des Benchmark-Codes:
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class ReturnEnumObjectVersusPrimitiveBenchmark {
enum Category {
CATEGORY1,
CATEGORY2,
}
@Param( {"3", "2", "1" })
String value;
int param;
@Setup
public void setUp() {
param = Integer.parseInt(value);
}
@Benchmark
public int benchmarkReturnOrdinal() {
if (param < 2) {
return Category.CATEGORY1.ordinal();
}
return Category.CATEGORY2.ordinal();
}
@Benchmark
public Category benchmarkReturnReference() {
if (param < 2) {
return Category.CATEGORY1;
}
return Category.CATEGORY2;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(ReturnEnumObjectVersusPrimitiveBenchmark.class.getName()).warmupIterations(5)
.measurementIterations(4).forks(1).build();
new Runner(opt).run();
}
}
Die Benchmark-Ergebnisse für oben:
# VM invoker: C:\Program Files\Java\jdk1.7.0_40\jre\bin\java.exe
# VM options: -Dfile.encoding=UTF-8
Benchmark (value) Mode Samples Score Error Units
benchmarkReturnOrdinal 3 thrpt 4 1059.898 ± 71.749 ops/us
benchmarkReturnOrdinal 2 thrpt 4 1051.122 ± 61.238 ops/us
benchmarkReturnOrdinal 1 thrpt 4 1064.067 ± 90.057 ops/us
benchmarkReturnReference 3 thrpt 4 353.197 ± 25.946 ops/us
benchmarkReturnReference 2 thrpt 4 350.902 ± 19.487 ops/us
benchmarkReturnReference 1 thrpt 4 339.578 ± 144.093 ops/us
Durch einfaches Ändern des Rückgabetyps der Funktion wurde die Leistung um den Faktor 3 geändert.
Ich dachte, dass der einzige Unterschied zwischen der Rückgabe eines Enum-Objekts und einer Ganzzahl darin besteht, dass einer einen 64-Bit-Wert (Referenz) und der andere einen 32-Bit-Wert zurückgibt. Einer meiner Kollegen vermutete, dass die Rückgabe der Aufzählung zusätzlichen Aufwand verursacht, da die Referenz für potenzielle GC nachverfolgt werden muss. (Angesichts der Tatsache, dass Enum-Objekte statische Endreferenzen sind, erscheint es seltsam, dass dies erforderlich wäre.)
Was ist die Erklärung für den Leistungsunterschied?
AKTUALISIEREN
Ich habe das Maven-Projekt hier geteilt, damit jeder es klonen und den Benchmark ausführen kann. Wenn jemand die Zeit / das Interesse hat, wäre es hilfreich zu sehen, ob andere die gleichen Ergebnisse replizieren können. (Ich habe auf zwei verschiedenen Computern, Windows 64 und Linux 64, repliziert, beide mit Oracle Java 1.7-JVMs). @ ZhekaKozlov sagt, er habe keinen Unterschied zwischen den Methoden gesehen.
So führen Sie Folgendes aus: (nach dem Klonen des Repositorys)
mvn clean install
java -jar .\target\microbenchmarks.jar function.ReturnEnumObjectVersusPrimitiveBenchmark -i 5 -wi 5 -f 1
quelle
Antworten:
TL; DR: Sie sollten BLIND kein Vertrauen in irgendetwas setzen.
Das Wichtigste zuerst: Es ist wichtig, die experimentellen Daten zu überprüfen, bevor Sie zu den daraus resultierenden Schlussfolgerungen gelangen. Nur zu behaupten, etwas sei dreimal schneller / langsamer, ist seltsam, weil Sie den Grund für den Leistungsunterschied wirklich nachverfolgen müssen und nicht nur den Zahlen vertrauen müssen. Dies ist besonders wichtig für Nano-Benchmarks wie Sie.
Zweitens sollten die Experimentatoren klar verstehen, was sie kontrollieren und was nicht. In Ihrem speziellen Beispiel geben Sie den Wert von
@Benchmark
Methoden zurück. Können Sie jedoch ziemlich sicher sein, dass die Aufrufer außerhalb dasselbe für das Grundelement und die Referenz tun? Wenn Sie sich diese Frage stellen, werden Sie feststellen, dass Sie im Grunde die Testinfrastruktur messen.Auf den Punkt gebracht. Auf meinem Computer (i5-4210U, Linux x86_64, JDK 8u40) ergibt der Test:
Benchmark (value) Mode Samples Score Error Units ...benchmarkReturnOrdinal 3 thrpt 5 0.876 ± 0.023 ops/ns ...benchmarkReturnOrdinal 2 thrpt 5 0.876 ± 0.009 ops/ns ...benchmarkReturnOrdinal 1 thrpt 5 0.832 ± 0.048 ops/ns ...benchmarkReturnReference 3 thrpt 5 0.292 ± 0.006 ops/ns ...benchmarkReturnReference 2 thrpt 5 0.286 ± 0.024 ops/ns ...benchmarkReturnReference 1 thrpt 5 0.293 ± 0.008 ops/ns
Okay, Referenztests erscheinen also dreimal langsamer. Aber warten Sie, es wird ein altes JMH (1.1.1) verwendet. Lassen Sie uns auf das neueste Update (1.7.1) aktualisieren:
Benchmark (value) Mode Cnt Score Error Units ...benchmarkReturnOrdinal 3 thrpt 5 0.326 ± 0.010 ops/ns ...benchmarkReturnOrdinal 2 thrpt 5 0.329 ± 0.004 ops/ns ...benchmarkReturnOrdinal 1 thrpt 5 0.329 ± 0.004 ops/ns ...benchmarkReturnReference 3 thrpt 5 0.288 ± 0.005 ops/ns ...benchmarkReturnReference 2 thrpt 5 0.288 ± 0.005 ops/ns ...benchmarkReturnReference 1 thrpt 5 0.288 ± 0.002 ops/ns
Ups, jetzt sind sie kaum noch langsamer. Übrigens sagt uns dies auch, dass der Test an die Infrastruktur gebunden ist. Okay, können wir sehen, was wirklich passiert?
Wenn Sie die Benchmarks erstellen und sich umschauen, was genau Ihre
@Benchmark
Methoden nennt , sehen Sie Folgendes:public void benchmarkReturnOrdinal_thrpt_jmhStub(InfraControl control, RawResults result, ReturnEnumObjectVersusPrimitiveBenchmark_jmh l_returnenumobjectversusprimitivebenchmark0_0, Blackhole_jmh l_blackhole1_1) throws Throwable { long operations = 0; long realTime = 0; result.startTime = System.nanoTime(); do { l_blackhole1_1.consume(l_longname.benchmarkReturnOrdinal()); operations++; } while(!control.isDone); result.stopTime = System.nanoTime(); result.realTime = realTime; result.measuredOps = operations; }
Das
l_blackhole1_1
hat eineconsume
Methode, die die Werte "verbraucht" (sieheBlackhole
Begründung).Blackhole.consume
hat Überladungen für Referenzen und Grundelemente , und das allein reicht aus, um den Leistungsunterschied zu rechtfertigen.Es gibt einen Grund, warum diese Methoden anders aussehen: Sie versuchen, für ihre Argumentationstypen so schnell wie möglich zu sein. Sie weisen nicht unbedingt die gleichen Leistungsmerkmale auf, obwohl wir versuchen, sie anzupassen, daher das symmetrischere Ergebnis mit neueren JMH. Jetzt können Sie sogar
-prof perfasm
den generierten Code für Ihre Tests anzeigen und sehen, warum die Leistung unterschiedlich ist, aber das geht hier über den Punkt hinaus.Wenn Sie wirklich verstehen möchten , wie sich die Rückgabe des Grundelements und / oder der Referenz in Bezug auf die Leistung unterscheidet, müssen Sie eine große, beängstigende Grauzone für nuanciertes Leistungsbenchmarking betreten . ZB so etwas wie dieser Test:
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(5) public class PrimVsRef { @Benchmark public void prim() { doPrim(); } @Benchmark public void ref() { doRef(); } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private int doPrim() { return 42; } @CompilerControl(CompilerControl.Mode.DONT_INLINE) private Object doRef() { return this; } }
... was für Primitive und Referenzen das gleiche Ergebnis liefert:
Benchmark Mode Cnt Score Error Units PrimVsRef.prim avgt 25 2.637 ± 0.017 ns/op PrimVsRef.ref avgt 25 2.634 ± 0.005 ns/op
Wie ich bereits sagte, diese Tests erfordern für die Ergebnisse zu den Gründen Verfolgung. In diesem Fall ist der generierte Code für beide fast gleich, und das erklärt das Ergebnis.
prim:
[Verified Entry Point] 12.69% 1.81% 0x00007f5724aec100: mov %eax,-0x14000(%rsp) 0.90% 0.74% 0x00007f5724aec107: push %rbp 0.01% 0.01% 0x00007f5724aec108: sub $0x30,%rsp 12.23% 16.00% 0x00007f5724aec10c: mov $0x2a,%eax ; load "42" 0.95% 0.97% 0x00007f5724aec111: add $0x30,%rsp 0.02% 0x00007f5724aec115: pop %rbp 37.94% 54.70% 0x00007f5724aec116: test %eax,0x10d1aee4(%rip) 0.04% 0.02% 0x00007f5724aec11c: retq
ref:
[Verified Entry Point] 13.52% 1.45% 0x00007f1887e66700: mov %eax,-0x14000(%rsp) 0.60% 0.37% 0x00007f1887e66707: push %rbp 0.02% 0x00007f1887e66708: sub $0x30,%rsp 13.63% 16.91% 0x00007f1887e6670c: mov %rsi,%rax ; load "this" 0.50% 0.49% 0x00007f1887e6670f: add $0x30,%rsp 0.01% 0x00007f1887e66713: pop %rbp 39.18% 57.65% 0x00007f1887e66714: test %eax,0xe3e78e6(%rip) 0.02% 0x00007f1887e6671a: retq
[Sarkasmus] Sehen Sie, wie einfach es ist! [/Sarkasmus]
Das Muster lautet: Je einfacher die Frage, desto mehr müssen Sie herausfinden, um eine plausible und zuverlässige Antwort zu erhalten.
quelle
Blackhole.consume
für den Benutzer auf. Sie können es wahrscheinlich in die@Benchmark
Methode ziehen und die nicht inlinierbare Methode verwenden, um die Ergebnisse zu verarbeiten, aber das funktioniert nur, bis Sie einen intelligenteren Optimierer treffen ... Während wir überdenken können, was JMH in diesem Fall verdeckt tut, würde der Benutzer hacken unvermeidlich zurückbleiben. Dann werden Benutzer, die mehr Vertrauen in ihren heiligen Code als in ein heiliges Benchmarking-Framework setzen, in der Hölle brennen!-Xint
, und die meisten Ihrer Benchmarking-Probleme verschwinden . (Bitte nicht) "Energieversorger hassen mich dafür." (c)Um das Missverständnis von Referenz und Speicher zu beseitigen, in das einige geraten sind (@Mzf), gehen wir auf die Java Virtual Machine-Spezifikation ein. Aber bevor man dorthin geht, muss eines geklärt werden: Ein Objekt kann niemals aus dem Speicher abgerufen werden, nur seine Felder können es . Tatsächlich gibt es keinen Opcode, der eine derart umfangreiche Operation ausführen würde.
In diesem Dokument wird die Referenz als Stapeltyp (so dass es sich um ein Ergebnis oder ein Argument für Anweisungen handeln kann, die Operationen am Stapel ausführen) der 1. Kategorie definiert - der Kategorie von Typen, die ein einzelnes Stapelwort (32 Bit) verwenden. Siehe Tabelle 2.3 .
Wenn der Methodenaufruf gemäß der Spezifikation normal abgeschlossen wird, wird ein Wert, der von der Oberseite des Stapels angezeigt wird, auf den Stapel des Methodenaufrufers verschoben (Abschnitt 2.6.4).
Ihre Frage ist, was den Unterschied der Ausführungszeiten verursacht. Kapitel 2 Vorwortantworten:
Mit anderen Worten, da im Dokument aus logischen Gründen keine Leistungsstrafe für die Verwendung von Referenzen angegeben ist (es handelt sich letztendlich nur um ein Stapelwort, wie
int
oder wiefloat
es ist), müssen Sie den Quellcode Ihrer Implementierung suchen oder nie überhaupt herausfinden.In gewissem Umfang sollten wir die Implementierung nicht immer beschuldigen. Es gibt einige Hinweise, die Sie bei der Suche nach Ihren Antworten verwenden können. Java definiert separate Anweisungen zum Bearbeiten von Zahlen und Referenzen. Referenz-Manipulation Anweisungen beginnen mit
a
(zBastore
,aload
oderareturn
) und sind die einzigen zur Arbeit mit Referenzen erlaubt Anweisungen. Insbesondere könnten Sie daran interessiert sein,areturn
die Implementierung zu betrachten.quelle
areturn
" macht keinen Sinn - so funktionieren moderne Compiler nicht (selbst der HotSpot-Interpreter interpretiert aus Leistungsgründen nicht mehr wirklich jeweils eine Anweisung)