Java Benchmarking mit Caliper, Maven und Java 7
Java August 23rd, 2011Bei Caliper handelt es sich um ein Framework um Mikrobenchmarks für Java Programme durchzuführen. Caliper ist bei Google code gehostet – http://code.google.com/p/caliper/ – und steht unter der Apache 2 Lizenz, kann somit sehr frei verwendet werden.
Caliper verfolgt einen ähnlichen Ansatz wie JUnit, Methoden müssen mit „time“ als Prefix benannt werden, und einen Parameter vom Typ int entgegennehmen um dann ausgeführt zu werden. Dieser Parameter gibt an, wie viele Iterationen der zu messenden Methode durchzuführen sind.
Leider ist Caliper bisher noch nicht in einer Version 1.0 released worden, daher bleibt nur selber bauen. Derzeit ist das jedoch auch nicht ganz einfach: Caliper haengt von Google Guava ab, nutzt in der svn-trunk Version dazu auch bereits Features von Guava r10, was auch noch nicht released ist. Also auch hier: Selber bauen, und die Abhängigkeit in Caliper korrigieren. Mein favorisiertes Build Tool ist in diesem Fall maven.
Möchte man noch während des Benchmarks die Speichernutzung analysieren, so bietet Caliper dies auch an – über einen Java Agent der von dem Google Projekt java-allocation-instrumenter (http://code.google.com/p/java-allocation-instrumenter/, Apache 2 Lizenz) bereitgestellt wird. (Leider ist hier derzeit der Maven Build nicht so konfiguriert, dass als Artefakt ein nutzbarer Java Agent erzeugt wird. Dies ist im Ant basierten Build in Ordnung, und kann mit einem kleinen Patch der Maven Konfiguration auch behoben werden.)
Der letzte Wehrmutstropfen bleibt nun, dass Caliper gerne eine Environment Variable, statt bspw. einem System Property, hätte, um den zu nutzenden Java-Agent zu spezifizieren – diese lässt sich bei Ausführung als Script zwar einfach setzten, jedoch aus einer Java Anwendung heraus eigentlich nicht. Dazu später mehr.)
Nach dem groben Überblick nun die Schritte im einzelnen:
Caliper per SVN auschecken:
svn checkout http://caliper.googlecode.com/svn/trunk/ caliper
Danach ändert man die pom.xml folgendermassen ab:
--- caliper/pom.xml (revision 330) +++ caliper/pom.xml (working copy) @@ -46,13 +46,13 @@ <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> - <version>r08</version> + <version>r10-SNAPSHOT</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.code.java-allocation-instrumenter</groupId> <artifactId>java-allocation-instrumenter</artifactId> - <version>2.0</version> + <version>2.1-SNAPSHOT</version>
Wie zu sehen ist, werden damit die Abhängigkeiten auf die (gleich ebenfalls noch zu erstellenden) lokalen Bibliotheken umgestellt.
Nun wird Guava lokal gebaut, um die aktuelle Version zur Verfuegung zu stellen. (-SNAPSHOT Artefakte werden nicht vom Maven „Central“ Repository angeboten, darum erstellen wir diese lokale selber.)
git clone https://code.google.com/p/guava-libraries/
cd guava-libraries
mvn clean install
Das war schmerzlos.
Nun kommen wir noch zu dem java-allocation-instrumenter. Dieser ist in Version 2.0 released und auch über Maven Central verfügbar. Möchte man dies nicht selber bauen, kann man auch das Manifest in der Jar Datei patchen, ich hab mich jedoch für einen Patch der Maven Konfiguration und anschließendes neubauen entschieden.
svn checkout http://java-allocation-instrumenter.googlecode.com/svn/trunk/ java-allocation-instrumenter
Danach wird die pom.xml folgendermassen geaendert:
--- java-allocation-instrumenter/pom.xml (revision 17) +++ java-allocation-instrumenter/pom.xml (working copy) @@ -83,6 +83,24 @@ <build> <defaultGoal>package</defaultGoal> <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>2.3.1</version> + <configuration> + <archive> + <index>true</index> + <manifest> + <addClasspath>true</addClasspath> + </manifest> + <manifestEntries> + <Premain-Class> com.google.monitoring.runtime.instrumentation.AllocationInstrumenter </Premain-Class> + <Can-Redefine-Classes>true</Can-Redefine-Classes> + <Main-Class>NotSuitableAsMain</Main-Class> + </manifestEntries> + </archive> + </configuration> + </plugin>
Fehlt dieser Eintrag, so schlaegt die Nutzung als Java-Agent fehlt, und man erhaelt so eine Meldung:
Failed to find Premain-Class manifest attribute from ...
Error occurred during initialization of VM
agent library failed to init: instrument
und das Programm bricht ab.
Nach dem Patchen wird nun das java-allocation-instrument auch übersetzt: „mvn clean install
„. Nun kann auch caliper uebersetzt werden, auch hier kommt der bekannte Maven Aufruf zum Einsatz.
Nun gilt es noch eine kleine Hürde im Zusammenhang mit Java 7 zu umschiffen: Nutzt man die Instrumentierung um Informationen über die Speichernutzung während des Benchmarks zu sammeln, so bricht die JVM die Ausführung mit der Meldung
Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target 32 in method com.google.caliper.InProcessRunner.main([Ljava/lang/String;)V at offset 0
ab. Der Fehler ist im Zusammenhang mit Java 7 und der verwendeten ASM Library zur Instrumentierung bekannt und sollte eigentlich bereits mit ASM 3.3.1 behoben sein. Als Workaround kann die Verifizierung der JVM deaktiviert werden, „-XX:-UseSplitVerifier“ ist die dazu nötige JVM Option.
Im Folgenden habe zwei Mini-Klassen, die die Verwendung von Caliper demonstrieren.
Zuerst die Benchmark Klasse, hier werden verschiedene Sortieralgorithmen gegeneinander getestet. (Die Implementierung überlasse ich dem geneigten Leser, lediglich Arrays.sort kommt direkt aus der Java Klassenbibliothek)
public class BenchmarkDemo extends SimpleBenchmark { @Param int size; //injected by caliper with -Dsize=1,2 private int[] values; @Override protected void setUp() throws Exception { values = new int[size]; Random generator = new Random(); for (int i = 0; i < values.length; i++) { values[i] = generator.nextInt(Integer.MAX_VALUE); } } public void timeHeapSort(int reps) { for (int i = 0; i < reps; i++) { HeapSort.sort(values); } } public void timeMergeSort(int reps) { for (int i = 0; i < reps; i++) { MergeSort.sort(values); } } public void timeQuickSort(int reps) { for (int i = 0; i < reps; i++) { QuickSort.sort(values); } } public void timeArraysSort(int reps) { for (int i = 0; i < reps; i++) { Arrays.sort(values); } } }
Da ich gerne aus der IDE (Netbeans in meinem Fall) heraus Caliper programmatisch starten möchte, habe ich eine Klasse erstellt, die sich darum kümmert.
Etwas kompliziert war an der Stelle, dass der Java-Agent nicht über ein System Property gesetzt werden kann, sondern lediglich per Environment-Variable. Diese sind in Java eigentlich nicht änderbar, mit etwas Reflection klappte es jedoch bei mir. (Robust ist das sicherlich nicht.) Auch verlasse ich mich an der Stelle darauf, dass der Agent per Maven im Standardpfad für Artefakte im lokalen Repository zur Verfügung steht. (Alternativ kann man per Environment-Variable den richtigen Pfad zum Agent weisen.)
package de.trion.caliperdemo; import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; import java.util.Map; public class BenchmarkRunner { public static void main(String[] args) throws Exception { //runBenchmark(); runBenchmarkWithMemoryInstrumentation(); } private static void runBenchmark() { com.google.caliper.Runner.main("-Dsize=100,1000,10000,100000", "--trials", "20", "de.trion.caliperdemo.BenchmarkDemo"); } private static void runBenchmarkWithMemoryInstrumentation() throws Exception { if (System.getenv("ALLOCATION_JAR") == null) { Map environment = new HashMap(System.getenv()); //you might want to replace this or setup an environment entry correctly String allocationAgent = "/.m2/repository/com/google/code/java-allocation-instrumenter"+ "/java-allocation-instrumenter/2.1-SNAPSHOT/java-allocation-instrumenter-2.1-SNAPSHOT.jar"; environment.put("ALLOCATION_JAR", System.getProperty("user.home") + allocationAgent); setEnvironmentHackOld(environment); } //JDK 7 instrumentation issue: -XX:-UseSplitVerifier com.google.caliper.Runner.main("-JjdkSevenFix=-XX:-UseSplitVerifier", "-Dsize=100,1000,10000,100000", "--trials", "20", "--measureMemory", "de.trion.caliperdemo.BenchmarkDemo"); } public static void setEnvironmentHackOld(Map newenv) throws Exception { Class[] classes = Collections.class.getDeclaredClasses(); Map env = System.getenv(); for (Class cl : classes) { if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { Field field = cl.getDeclaredField("m"); field.setAccessible(true); Object obj = field.get(env); @SuppressWarnings("unchecked") Map map = (Map) obj; map.clear(); map.putAll(newenv); } } } }
Wie man gut sehen kann, kann Caliper System-Properties, wie in diesem Fall „size“, verwenden um einen Benchmark zu parametrisieren. Das Pendant dazu findet sich im Quellcode:
@Param int size;
Auch können mehrere Parameter als Liste definiert werden und damit lässt sich in diesem Fall schön sehen, wie sich die verschiedenen Sortierverfahren bei steigender Größe verhalten.
Die Nummer der Durchführungen (trials) dient dazu, einen gemittelten Wert zu erhalten um temporäre Effekte mit Einfluss auf den Benchmark zu verringern.
Natürlich lässt sich caliper äquivalent auch durch ein Script oder von der Kommandozeile aus aufrufen, weitere Hinweise und Tutorials finden sich auf der Homepage – und einiges lediglich im Caliper Quellcode.
Neue Kommentare