2016-07-31 31 views
10

Ich schrieb einen JMH-Benchmark mit 2 Methoden: M1 und M2. M1 ruft M2 auf, aber aus irgendeinem Grund behauptet JMH, dass M1 schneller ist als M2.jmh zeigt an, dass M1 schneller ist als M2, aber M1 delegiert an M2

Hier ist der Benchmark-Quelle-Code:

import java.util.concurrent.TimeUnit; 
import static org.bitbucket.cowwoc.requirements.Requirements.assertThat; 
import static org.bitbucket.cowwoc.requirements.Requirements.requireThat; 
import org.openjdk.jmh.annotations.Benchmark; 
import org.openjdk.jmh.annotations.BenchmarkMode; 
import org.openjdk.jmh.annotations.Mode; 
import org.openjdk.jmh.annotations.OutputTimeUnit; 
import org.openjdk.jmh.runner.Runner; 
import org.openjdk.jmh.runner.RunnerException; 
import org.openjdk.jmh.runner.options.Options; 
import org.openjdk.jmh.runner.options.OptionsBuilder; 

@BenchmarkMode(Mode.AverageTime) 
@OutputTimeUnit(TimeUnit.NANOSECONDS) 
public class MyBenchmark { 

    @Benchmark 
    public void assertMethod() { 
     assertThat("value", "name").isNotNull().isNotEmpty(); 
    } 

    @Benchmark 
    public void requireMethod() { 
     requireThat("value", "name").isNotNull().isNotEmpty(); 
    } 

    public static void main(String[] args) throws RunnerException { 
     Options opt = new OptionsBuilder() 
       .include(MyBenchmark.class.getSimpleName()) 
       .forks(1) 
       .build(); 

     new Runner(opt).run(); 
    } 
} 

In dem obigen Beispiel M1 assertThat() ist, M2 ist requireThat(). Bedeutung, assertThat() ruft requireThat() unter der Haube auf. Hier

ist der Maßstab Ausgabe:

# JMH 1.13 (released 8 days ago) 
# VM version: JDK 1.8.0_102, VM 25.102-b14 
# VM invoker: C:\Program Files\Java\jdk1.8.0_102\jre\bin\java.exe 
# VM options: -ea 
# Warmup: 20 iterations, 1 s each 
# Measurement: 20 iterations, 1 s each 
# Timeout: 10 min per iteration 
# Threads: 1 thread, will synchronize iterations 
# Benchmark mode: Average time, time/op 
# Benchmark: com.mycompany.jmh.MyBenchmark.assertMethod 

# Run progress: 0.00% complete, ETA 00:01:20 
# Fork: 1 of 1 
# Warmup Iteration 1: 8.268 ns/op 
# Warmup Iteration 2: 6.082 ns/op 
# Warmup Iteration 3: 4.846 ns/op 
# Warmup Iteration 4: 4.854 ns/op 
# Warmup Iteration 5: 4.834 ns/op 
# Warmup Iteration 6: 4.831 ns/op 
# Warmup Iteration 7: 4.815 ns/op 
# Warmup Iteration 8: 4.839 ns/op 
# Warmup Iteration 9: 4.825 ns/op 
# Warmup Iteration 10: 4.812 ns/op 
# Warmup Iteration 11: 4.806 ns/op 
# Warmup Iteration 12: 4.805 ns/op 
# Warmup Iteration 13: 4.802 ns/op 
# Warmup Iteration 14: 4.813 ns/op 
# Warmup Iteration 15: 4.805 ns/op 
# Warmup Iteration 16: 4.818 ns/op 
# Warmup Iteration 17: 4.815 ns/op 
# Warmup Iteration 18: 4.817 ns/op 
# Warmup Iteration 19: 4.812 ns/op 
# Warmup Iteration 20: 4.810 ns/op 
Iteration 1: 4.805 ns/op 
Iteration 2: 4.816 ns/op 
Iteration 3: 4.813 ns/op 
Iteration 4: 4.938 ns/op 
Iteration 5: 5.061 ns/op 
Iteration 6: 5.129 ns/op 
Iteration 7: 4.828 ns/op 
Iteration 8: 4.837 ns/op 
Iteration 9: 4.819 ns/op 
Iteration 10: 4.815 ns/op 
Iteration 11: 4.872 ns/op 
Iteration 12: 4.806 ns/op 
Iteration 13: 4.811 ns/op 
Iteration 14: 4.827 ns/op 
Iteration 15: 4.837 ns/op 
Iteration 16: 4.842 ns/op 
Iteration 17: 4.812 ns/op 
Iteration 18: 4.809 ns/op 
Iteration 19: 4.806 ns/op 
Iteration 20: 4.815 ns/op 


Result "assertMethod": 
    4.855 �(99.9%) 0.077 ns/op [Average] 
    (min, avg, max) = (4.805, 4.855, 5.129), stdev = 0.088 
    CI (99.9%): [4.778, 4.932] (assumes normal distribution) 


# JMH 1.13 (released 8 days ago) 
# VM version: JDK 1.8.0_102, VM 25.102-b14 
# VM invoker: C:\Program Files\Java\jdk1.8.0_102\jre\bin\java.exe 
# VM options: -ea 
# Warmup: 20 iterations, 1 s each 
# Measurement: 20 iterations, 1 s each 
# Timeout: 10 min per iteration 
# Threads: 1 thread, will synchronize iterations 
# Benchmark mode: Average time, time/op 
# Benchmark: com.mycompany.jmh.MyBenchmark.requireMethod 

# Run progress: 50.00% complete, ETA 00:00:40 
# Fork: 1 of 1 
# Warmup Iteration 1: 7.193 ns/op 
# Warmup Iteration 2: 4.835 ns/op 
# Warmup Iteration 3: 5.039 ns/op 
# Warmup Iteration 4: 5.053 ns/op 
# Warmup Iteration 5: 5.077 ns/op 
# Warmup Iteration 6: 5.102 ns/op 
# Warmup Iteration 7: 5.088 ns/op 
# Warmup Iteration 8: 5.109 ns/op 
# Warmup Iteration 9: 5.096 ns/op 
# Warmup Iteration 10: 5.096 ns/op 
# Warmup Iteration 11: 5.091 ns/op 
# Warmup Iteration 12: 5.089 ns/op 
# Warmup Iteration 13: 5.099 ns/op 
# Warmup Iteration 14: 5.097 ns/op 
# Warmup Iteration 15: 5.090 ns/op 
# Warmup Iteration 16: 5.096 ns/op 
# Warmup Iteration 17: 5.088 ns/op 
# Warmup Iteration 18: 5.086 ns/op 
# Warmup Iteration 19: 5.087 ns/op 
# Warmup Iteration 20: 5.097 ns/op 
Iteration 1: 5.097 ns/op 
Iteration 2: 5.088 ns/op 
Iteration 3: 5.092 ns/op 
Iteration 4: 5.097 ns/op 
Iteration 5: 5.082 ns/op 
Iteration 6: 5.089 ns/op 
Iteration 7: 5.086 ns/op 
Iteration 8: 5.084 ns/op 
Iteration 9: 5.090 ns/op 
Iteration 10: 5.086 ns/op 
Iteration 11: 5.084 ns/op 
Iteration 12: 5.088 ns/op 
Iteration 13: 5.091 ns/op 
Iteration 14: 5.092 ns/op 
Iteration 15: 5.085 ns/op 
Iteration 16: 5.096 ns/op 
Iteration 17: 5.078 ns/op 
Iteration 18: 5.125 ns/op 
Iteration 19: 5.089 ns/op 
Iteration 20: 5.091 ns/op 


Result "requireMethod": 
    5.091 �(99.9%) 0.008 ns/op [Average] 
    (min, avg, max) = (5.078, 5.091, 5.125), stdev = 0.010 
    CI (99.9%): [5.082, 5.099] (assumes normal distribution) 


# Run complete. Total time: 00:01:21 

Benchmark      Mode Cnt Score Error Units 
MyBenchmark.assertMethod  avgt 20 4.855 � 0.077 ns/op 
MyBenchmark.requireMethod  avgt 20 5.091 � 0.008 ns/op 

diese lokal zu reproduzieren:

  1. erstellen Maven Projekt des über den Benchmark enthält.
  2. die folgende Abhängigkeit hinzufügen:

    <dependency> 
        <groupId>org.bitbucket.cowwoc</groupId> 
        <artifactId>requirements</artifactId> 
        <version>2.0.0</version> 
    </dependency> 
    
  3. Alternativ laden Sie die Bibliothek von https://bitbucket.org/cowwoc/requirements/

ich folgende Fragen:

  1. Können Sie dieses Ergebnis auf Ihrem Ende reproduzieren?
  2. Was ist mit dem Benchmark falsch?

UPDATE: Ich stellte einen aktualisierte Benchmark-Source-Code, Benchmark-Ausgang, jmh-Testausgang und xperfasm Ausgang https://bitbucket.org/cowwoc/requirements/downloads pro Aleksey Shipilev ‚s Vorschlag. Ich kann diese nicht an Stackoverflow senden, da die Anzahl der Fragen auf 30.000 Zeichen begrenzt ist.

UPDATE2: Ich bekomme endlich konsistente, aussagekräftige Ergebnisse.

Benchmark     Mode Cnt Score Error Units 
MyBenchmark.assertMethod avgt 60 22.552 ± 0.020 ns/op 
MyBenchmark.requireMethod avgt 60 22.411 ± 0.114 ns/op 

von consistent, ich meine, dass ich fast die gleichen Werte für Läufe erhalten.

Durch meaningful, ich meine, dass assertMethod() langsamer ist als requireMethod().

ich folgende Änderungen vorgenommen:

  • Gesperrt den CPU-Takt (min/max CPU Satz auf 99% in Energieoptionen von Windows)
  • Added Optionen JVM -XX:-TieredCompilation -XX:-ProfileInterpreter

Ist jemand in der Lage um diese Ergebnisse ohne die Verdoppelung der Laufzeiten zu erreichen?

UPDATE3: Das Deaktivieren von Inlining führt zu den gleichen Ergebnissen ohne merkliche Leistungsabschwächung. Ich habe eine ausführlichere Antwort here gepostet.

+1

Dies mag nicht das Problem sein, aber Sie sollten Eingaben von '@ State'-Feldern erhalten und die Ausgaben an' @ Benchmark' return value oder an das explizite 'Blackhole' versenken. Siehe: https://gist.github.com/shipilev/712b5e4e4800c3ed982dcbae97e4d6df –

+1

Ich kann den Unterschied in der ähnlichen Konfiguration sowieso nicht reproduzieren, was einige Zweifel an der Gültigkeit der Umgebungseinstellungen aufkommen lässt. Versuchen Sie JMH Core Benchmark Tests durchzuführen? Runnable JAR: http://central.maven.org/maven2/org/openjdk/jmh/jmh-core-benchmarks/1.13/jmh-core-benchmarks--1.13-full.jar –

+0

@AlekseyShipilev Guter Fang über das Nicht-Finale Eingänge und Sinken der Ausgabe. Ich bekomme jetzt realistischere Zahlen (siehe ** UPDATE2 **), aber 'assertMethod()' ist immer noch schneller als 'requireMethod()'. Ich habe auch die JMH Core Benchmarktests durchgeführt, wie Sie erwähnt haben, und sie scheinen wieder normal zu sein.Ich kann das Ergebnis nicht auf Stackoverflow aufgrund der 30k Zeichen Grenze veröffentlichen, aber wenn Sie mir eine E-Mail senden (siehe mein Profil, um meine E-Mail-Adresse zu erhalten), schicke ich Ihnen die Datei. – Gili

Antwort

10

In diesem speziellen Fall assertMethod ist tatsächlich kompiliert besser als requireMethod aufgrund von Problemen bei der Registrierung von Registern.

Der Benchmark sieht korrekt aus und ich kann Ihre Ergebnisse konsistent reproduzieren.
Um zu analysieren, um das Problem ich the simplified benchmark gemacht habe:

package bench; 

import com.google.common.collect.ImmutableMap; 
import org.openjdk.jmh.annotations.*; 

@State(Scope.Benchmark) 
public class Requirements { 
    private static boolean enabled = true; 

    private String name = "name"; 
    private String value = "value"; 

    @Benchmark 
    public Object assertMethod() { 
     if (enabled) 
      return requireThat(value, name); 
     return null; 
    } 

    @Benchmark 
    public Object requireMethod() { 
     return requireThat(value, name); 
    } 

    public static Object requireThat(String parameter, String name) { 
     if (name.trim().isEmpty()) 
      throw new IllegalArgumentException(); 
     return new StringRequirementsImpl(parameter, name, new Configuration()); 
    } 

    static class Configuration { 
     private Object context = ImmutableMap.of(); 
    } 

    static class StringRequirementsImpl { 
     private String parameter; 
     private String name; 
     private Configuration config; 
     private ObjectRequirementsImpl asObject; 

     StringRequirementsImpl(String parameter, String name, Configuration config) { 
      this.parameter = parameter; 
      this.name = name; 
      this.config = config; 
      this.asObject = new ObjectRequirementsImpl(parameter, name, config); 
     } 
    } 

    static class ObjectRequirementsImpl { 
     private Object parameter; 
     private String name; 
     private Configuration config; 

     ObjectRequirementsImpl(Object parameter, String name, Configuration config) { 
      this.parameter = parameter; 
      this.name = name; 
      this.config = config; 
     } 
    } 
} 

Zunächst einmal ich von -XX:+PrintInlining, dass die ganze Benchmark in eine große Methode inlined wird überprüft haben. Offensichtlich hat diese Kompilierungseinheit viele Knoten und es sind nicht genügend CPU-Register vorhanden, um alle Zwischenvariablen zu speichern. Das heißt, Compiler muss einige von ihnen zu spill.

  • In assertMethod4 registers werden auf den Stapel vor dem Aufruf von trim() verschüttet.
  • In requireMethod7 registers werden später, nach dem Anruf an new Configuration() verschüttet.

-XX:+PrintAssembly Ausgang:

assertMethod    | requireMethod 
    -------------------------|------------------------ 
    mov %r11d,0x5c(%rsp) | mov %rcx,0x20(%rsp) 
    mov %r10d,0x58(%rsp) | mov %r11,0x48(%rsp) 
    mov %rbp,0x50(%rsp) | mov %r10,0x30(%rsp) 
    mov %rbx,0x48(%rsp) | mov %rbp,0x50(%rsp) 
          | mov %r9d,0x58(%rsp) 
          | mov %edi,0x5c(%rsp) 
          | mov %r8,0x60(%rsp) 

Dies ist fast der einzige Unterschied zwischen zwei kompilierte Verfahren zusätzlich zu if (enabled) überprüfen. Der Leistungsunterschied wird also durch mehr Variablen erklärt, die in den Speicher ausgelaufen sind.

Warum ist die kleinere Methode weniger optimal zusammengestellt? Nun, das Problem der Registerzuordnung ist bekanntlich NP-vollständig. Da es in einer angemessenen Zeit nicht ideal gelöst werden kann, verlassen sich Compiler normalerweise auf bestimmte Heuristiken. In einer großen Methode kann ein kleines Ding wie ein extra if das Ergebnis des Algorithmus zur Registerzuweisung signifikant ändern.

Allerdings müssen Sie sich nicht darum kümmern. Der Effekt, den wir gesehen haben, bedeutet nicht, dass requireMethod immer schlechter kompiliert wird. In anderen Anwendungsfällen wird das Kompilierungsdiagramm aufgrund von Inlining völlig anders sein. Jedenfalls ist eine Differenz von 1 Nanosekunde nichts für die tatsächliche Anwendungsleistung.

+0

Endlich eine Antwort, die Sinn macht! :) Besonders gefällt mir, dass Sie das Problem mit einem vereinfachten Benchmark reproduzieren konnten. Gut gemacht! Bitte versuche, so viel wie möglich in diese Antwort zu bringen, um in Zukunft fehlerhafte Links zu vermeiden. – Gili

2

Sie führen Ihren Test innerhalb eines einzelnen VM-Prozesses aus, indem Sie forks(1) angeben. Während der Laufzeit untersucht eine virtuelle Maschine Ihren Code und versucht herauszufinden, wie er ausgeführt wird. Es erstellt dann sogenannte Profile, um Ihre Anwendung entsprechend diesem beobachteten Verhalten zu optimieren.

Was hier am wahrscheinlichsten passiert, heißt Profilverschmutzung. Hier wirkt sich der Start der ersten Benchmark auf das Ergebnis der zweiten Benchmark aus. Überflüssig vereinfacht: Wenn Ihre VM so trainiert wurde (a), dass sie ihren Benchmark ausführt, dauert es etwas mehr Zeit, sich daran zu gewöhnen (b) danach. Daher scheint (b) mehr Zeit zu benötigen.

Um dies zu vermeiden, führen Sie Ihren Benchmark mit mehreren Gabeln aus, in denen die verschiedenen Benchmarks auf frischen VM-Prozessen ausgeführt werden, um eine solche Profilverschmutzung zu vermeiden. In den Beispielen are provided by JMH können Sie mehr über Forking lesen.

Sie sollten auch überprüfen the sample on state; Sie sollten nicht auf Ihre Eingabe als Konstanten verweisen, sondern JMH die Escape-Anweisung des Werts behandeln, um eine tatsächliche Berechnung anzuwenden.

Ich denke, dass - wenn richtig angewendet - beide Benchmarks ähnliche Laufzeit ergeben würden.

aktualisieren - Hier ist, was ich für die feste Benchmark erhalten:

Benchmark     Mode Cnt Score Error Units 
MyBenchmark.assertMethod avgt 40 17,592 ± 1,493 ns/op 
MyBenchmark.requireMethod avgt 40 17,999 ± 0,920 ns/op 

Aus Gründen der Fertigstellung, lief ich auch die Benchmark mit perfasm und werden beide Methoden im Grunde in der gleichen Sache zusammengestellt .

+0

Diese Antwort klang vielversprechend, aber weder Forking noch staatliche Isolation haben für mich funktioniert. Bitte versuchen Sie das Problem lokal zu reproduzieren. Wenn Sie in der Lage sind, es zu lösen, schreiben Sie bitte Arbeitscode, damit ich Ihre Ergebnisse zu meinem Ende bestätigen kann. – Gili

+0

Wie haben Sie Ihren Benchmark geändert? –

+0

Ich habe der Frage den aktualisierten Benchmark hinzugefügt. Schau mal. Ich würde mich freuen, wenn Sie in dieser Antwort Ihre eigene Implementierung veröffentlichen. – Gili

0

Dies ist sehr häufig in Microbenchmarks. Wenn ich deinen Code herunterlade, bekomme ich das gleiche Ergebnis, aber mit anderen Figuren ist mein Computer anscheinend langsamer als deiner. Wenn ich jedoch Ihre Quelle so ändere, dass 5 Forks, 100 Warmup-Iterationen und 20 Measurement-Iterationen verwendet werden, wird requiremethod erwartungsgemäß ein wenig schneller als assertMethod.

JMH ist großartig, aber es ist sehr einfach, Tests zu schreiben, die gut aussehen, aber denen Sie nicht vertrauen können, da es zu wenige Iterationen gibt.

+0

Sie hatten mich aufgeregt, aber ich konnte Ihr Ergebnis nicht reproduzieren (mit den gleichen Parametern, die Sie erwähnt haben). Ich habe dann versucht, so hoch wie 10 Gabeln, 100 Warm-up-Iterationen und 100 Mess Iterationen aber 'assertMethod()' noch lief schneller als 'requireMethod()' gehen. – Gili

1

meine eigene Frage zu beantworten:

Es scheint, dass inlining Ergebnisse ist Verkanten. Alles, was ich konsistente, aussagekräftige Ergebnisse war das folgende zu erhalten tun musste:

  • Sperren Sie den CPU-Takt (min/max CPU auf 99% in Energieoptionen von Windows)
  • Disable inlining durch beide Methoden mit Anmerkungen versehen mit @CompilerControl(CompilerControl.Mode.DONT_INLINE) .

ich jetzt folgende Ergebnisse erhalten:

Benchmark     Mode Cnt Score Error Units 
MyBenchmark.assertMethod avgt 200 11.462 ± 0.048 ns/op 
MyBenchmark.requireMethod avgt 200 11.138 ± 0.062 ns/op 

Ich habe versucht, die Ausgabe von -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining Analyse konnte aber nichts finden. Beide Methoden scheinen auf die gleiche Weise inline zu werden.<zucken>


Die Benchmark-Quelle-Code ist:

import java.util.concurrent.TimeUnit; 
import static org.bitbucket.cowwoc.requirements.Requirements.assertThat; 
import static org.bitbucket.cowwoc.requirements.Requirements.requireThat; 
import org.bitbucket.cowwoc.requirements.StringRequirements; 
import org.openjdk.jmh.annotations.Benchmark; 
import org.openjdk.jmh.annotations.CompilerControl; 
import org.openjdk.jmh.annotations.Mode; 
import org.openjdk.jmh.annotations.Scope; 
import org.openjdk.jmh.annotations.State; 
import org.openjdk.jmh.runner.Runner; 
import org.openjdk.jmh.runner.RunnerException; 
import org.openjdk.jmh.runner.options.Options; 
import org.openjdk.jmh.runner.options.OptionsBuilder; 

@State(Scope.Benchmark) 
public class MyBenchmark { 

    private String name = "name"; 
    private String value = "value"; 

    @Benchmark 
    public void emptyMethod() { 
    } 

    // Inlining leads to unexpected results: https://stackoverflow.com/a/38860869/14731 
    @Benchmark 
    @CompilerControl(CompilerControl.Mode.DONT_INLINE) 
    public StringRequirements assertMethod() { 
     return assertThat(value, name).isNotNull().isNotEmpty(); 
    } 

    @Benchmark 
    @CompilerControl(CompilerControl.Mode.DONT_INLINE) 
    public StringRequirements requireMethod() { 
     return requireThat(value, name).isNotNull().isNotEmpty(); 
    } 

    public static void main(String[] args) throws RunnerException { 
     Options opt = new OptionsBuilder() 
       .include(MyBenchmark.class.getSimpleName()) 
       .jvmArgsAppend("-ea") 
       .forks(3) 
       .timeUnit(TimeUnit.NANOSECONDS) 
       .mode(Mode.AverageTime) 
       .build(); 

     new Runner(opt).run(); 
    } 
} 

UPDATE: apangin scheint figured out zu haben, warum assertMethod() schneller als requireMethod() ist.