Java - Mikrobenchmarking: Ein Anfänger-Leitfaden

Hallo da, zukünftige Java-Zauberer! ? Heute werden wir auf eine aufregende Reise in die Welt des Java-Mikrobenchmarkings gehen. Keine Sorge, wenn du noch nie einen Codezeile geschrieben hast - wir beginnen bei den Grundlagen und arbeiten uns gemeinsam nach oben vor. Also, holen dir eine Tasse Kaffee (oder Tee, wenn das dein Ding ist) und lasst uns rein springen!

Java - Microbenchmark

Was ist Mikrobenchmarking?

Bevor wir in die Details des Java-Mikrobenchmarkings einsteigen, lassen Sie uns verstehen, was Mikrobenchmarking eigentlich ist.

Stell dir vor, du bist ein Küchenchef, der eine Rezeptur perfektionieren möchte. Du würdest nicht nur das endgültige Gericht probieren, um zu sehen, ob es gut ist, oder? Du würdest jede Zutat probieren, verschiedene Garzeiten testen und verschiedene Techniken ausprobieren. Genau das ist Mikrobenchmarking in der Programmierung - es ist eine Möglichkeit, die Leistung kleiner, isolierter Teile deines Codes zu messen.

Warum ist Java-Benchmarking wichtig?

Nun magst du dich fragen: "Warum sollte mir Benchmarking gefallen?" Na, lass mich dir eine kleine Geschichte erzählen.

Als ich noch ein Junior-Entwickler war, habe ich einmal ein Programm geschrieben, das perfekt funktioniert hat... auf meinem Computer. Aber als wir es auf die Firmenserver deployten, war es langsamer als eine Schildkröte, die einen schweren Rucksack trägt! Damals habe ich die Bedeutung des Benchmarkings gelernt. Es hilft uns:

  1. Leistungsengpässe zu identifizieren
  2. Verschiedene Implementierungen zu vergleichen
  3. Sicherzustellen, dass unser Code auf verschiedenen Systemen effizient läuft

Java-Benchmarking-Techniken

Lassen Sie uns einige gängige Java-Benchmarking-Techniken ansehen:

1. Manuelle Zeitnahme

Der einfachste Weg zum Benchmarking ist die manuelle Zeitnahme. Hier ist ein einfaches Beispiel:

public class SimpleTimingExample {
public static void main(String[] args) {
long startTime = System.nanoTime();

// Dein Code hier
for (int i = 0; i < 1000000; i++) {
Math.sqrt(i);
}

long endTime = System.nanoTime();
long duration = (endTime - startTime);
System.out.println("Ausführungszeit: " + duration + " Nanosekunden");
}
}

In diesem Beispiel verwenden wir System.nanoTime(), um zu messen, wie lange es dauert, die Quadratwurzel von Zahlen von 0 bis 999.999 zu berechnen.

2. Verwendung von JMH (Java Microbenchmark Harness)

Während die manuelle Zeitnahme einfach ist, ist sie nicht immer genau. Hier kommt JMH ins Spiel. JMH ist ein Java-Harness zum Erstellen, Ausführen und Analysieren von nano/micro/milli/macro Benchmarks.

Um JMH zu verwenden, musst du es deinem Projekt hinzufügen. Wenn du Maven verwendest, füge diese Abhängigkeiten zu deiner pom.xml hinzu:

<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
</dependencies>

Jetzt schreiben wir einen einfachen JMH-Benchmark:

import org.openjdk.jmh.annotations.*;
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;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 3)
@Measurement(iterations = 3)
public class JMHExample {

@Benchmark
public void benchmarkMathSqrt() {
Math.sqrt(143);
}

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

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

Dieser Benchmark misst die durchschnittliche Zeit, die es dauert, die Quadratwurzel von 143 zu berechnen. Lassen Sie uns die Annotationen auflisten:

  • @BenchmarkMode: Gibt an, was gemessen werden soll (durchschnittliche Zeit in diesem Fall)
  • @OutputTimeUnit: Gibt die Einheit für die Ergebnisse an
  • @State: Definiert den Bereich, in dem "Zustand"-Objekte geteilt werden
  • @Fork: Wie oft ein einzelner Benchmark geforkt wird
  • @Warmup und @Measurement: Definieren, wie viele Warmup- und Messdurchläufe durchgeführt werden

Java Collections-Algorithmen

Während wir noch beim Benchmarking sind, lassen wir uns einen kurzen Abstecher zu den Java Collections-Algorithmen machen. Diese sind unglaublich nützliche Tools, die die Leistung deines Programms erheblich beeinflussen können.

Hier ist eine Tabelle einiger gängiger Algorithmen:

Algorithm Beschreibung Anwendungsfall
Collections.sort() Sortiert eine Liste Wenn du die Elemente sortieren musst
Collections.binarySearch() Sucht in einer sortierten Liste Findet ein Element in einer großen, sortierten Liste
Collections.reverse() Kehrt eine Liste um Wenn du die Reihenfolge der Elemente umkehren musst
Collections.shuffle() _permutiert eine Liste zufällig Randomisiert die Reihenfolge der Elemente
Collections.fill() Ersetzt alle Elemente durch das angegebene Element Initialisiert eine Liste mit einem spezifischen Wert

Lassen Sie uns die Leistung des Sortierens einer Liste mit Collections.sort() benchmarken:

import org.openjdk.jmh.annotations.*;
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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 3)
@Measurement(iterations = 3)
public class SortingBenchmark {

@Param({"100", "1000", "10000"})
private int listSize;

private List<Integer> list;

@Setup
public void setup() {
list = new ArrayList<>(listSize);
Random rand = new Random();
for (int i = 0; i < listSize; i++) {
list.add(rand.nextInt());
}
}

@Benchmark
public void benchmarkCollectionsSort() {
Collections.sort(list);
}

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

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

Dieser Benchmark misst, wie lange es dauert, Listen unterschiedlicher Größe (100, 1000 und 10000 Elemente) zu sortieren. Wenn du dies ausführst, erhältst du eine gute Vorstellung davon, wie sich die Sortierzeit mit der Größe der Liste erhöht.

Fazit

Und so, meine Freunde, das war's! Wir haben nur die Oberfläche des Java-Mikrobenchmarkings angeritzt. Denke daran, Benchmarking ist nicht nur darum, schnellen Code zu schreiben - es geht darum, die Leistungseigenschaften deines Codes zu verstehen und informierte Entscheidungen zu treffen.

Während du deinen Java-Weg fortsetzt, behalte das Benchmarking in deinem Werkzeugkasten. Es ist wie ein vertrauenswürdiger Kompass, der dir helfen wird, die manchmal stürmischen Meere der Softwareleistung zu navigieren.

Happy coding, und möge dein Benchmarking immer aufschlussreich sein! ??‍??‍?

Credits: Image by storyset