From 20bd4629b38b2fc79e25aed917d7ed7a5d99e9f2 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Tue, 7 Jan 2025 17:54:06 +0000 Subject: [PATCH] benchmark: JMH IntelliJ plugin --- README.md | 21 +++++++++++++----- app/build.gradle.kts | 4 +++- .../powersort/benchmark/JmhBase.java | 6 ----- .../powersort/benchmark/JmhCgl.java | 9 ++++++++ .../powersort/benchmark/JmhCompetition.java | 6 +++++ intellij-jmh.png | Bin 0 -> 14199 bytes 6 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 intellij-jmh.png diff --git a/README.md b/README.md index e81cd12..023dc9d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This should include version >= 23, e.g.: ### Development Setup with nix -The provided [shell.nix](shell.nix) file can be used to set up a development environment with IntelliJ. The only prerequisite is to have a working `nix` installation for which there are multiple possibilities, e.g: +The provided [./shell.nix](./shell.nix) file can be used to set up a development environment with IntelliJ. The only prerequisite is to have a working `nix` installation for which there are multiple possibilities, e.g: ```shell # https://github.com/DeterminateSystems/nix-installer?tab=readme-ov-file#determinate-nix-installer @@ -90,26 +90,37 @@ There are two different benchmarks. One is based on the JMH benchmark framework, ### Custom -Run custom benchmark: +Run Custom Benchmark (CGM) with + +- Custom Generated Lists (CGL): ```shell ./gradlew runCbmCgl +``` + +- Powersort competition lists: + +```shell ./gradlew runCbmCompetition ``` ### JMH -Run JMH benchmark: +#### Run JMH with CGL and Powersort competition lists ```shell ./gradlew jmh ``` -IntelliJ plugin: +- To benchmark only one of the different list collections, see `jmh { excludes }` at the bottom of [./app/build.gradle.kts](./app/build.gradle.kts). + +#### IntelliJ plugin The [JMH Java Microbenchmark Harness](https://plugins.jetbrains.com/plugin/7529-jmh-java-microbenchmark-harness) plugin allows running benchmarks from within the IDE in a similar way as JUnit test cases. This can be used for development but yields less accurate results. -Further notes: +![intellij-jmh.png](intellij-jmh.png) + +#### Further notes - JHM: https://github.com/openjdk/jmh?tab=readme-ov-file#other-build-systems - We use the JMH Gradle plugin: https://github.com/melix/jmh-gradle-plugin diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1628776..6aadce9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,8 @@ jmh { forceGC = true excludes = listOf( - "de.uni_marburg.powersort.benchmark.JmhBase.benchmark", + // To skip JmhCgl or JmhCompetition, uncomment it below. +// "de.uni_marburg.powersort.benchmark.JmhCgl.benchmark", +// "de.uni_marburg.powersort.benchmark.JmhCompetition.benchmark", ) } diff --git a/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhBase.java b/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhBase.java index 237af37..f8b3e81 100644 --- a/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhBase.java +++ b/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhBase.java @@ -4,7 +4,6 @@ import de.uni_marburg.powersort.data.DataEnum; import de.uni_marburg.powersort.data.ObjectSupplier; import de.uni_marburg.powersort.sort.SortEnum; import de.uni_marburg.powersort.sort.SortImpl; -import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; @@ -75,9 +74,4 @@ public class JmhBase { // This way, all iterations sort the same input. workingCopy = data.getCopy(); } - - @Benchmark - public void benchmark() { - sortImpl.sort(workingCopy); - } } diff --git a/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCgl.java b/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCgl.java index ede8f4a..96d2355 100644 --- a/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCgl.java +++ b/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCgl.java @@ -3,6 +3,7 @@ package de.uni_marburg.powersort.benchmark; import de.uni_marburg.powersort.data.CglEnum; import de.uni_marburg.powersort.data.DataEnum; import de.uni_marburg.powersort.sort.SortEnum; +import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; @@ -28,4 +29,12 @@ public class JmhCgl extends JmhBase { SortEnum getSortEnum(){ return sortEnum; } + + // We could move this method up to JmhBase to avoid redundancy in the subclasses. + // But this would remove the run button of the IntelliJ plugin "JMH Java Microbenchmark Harness". + // Furthermore, with this approach we don't need to exclude JmhBase as it has no benchmark methods. + @Benchmark + public void benchmark() { + sortImpl.sort(workingCopy); + } } diff --git a/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCompetition.java b/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCompetition.java index f6b4cba..8d77a10 100644 --- a/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCompetition.java +++ b/app/src/jmh/java/de/uni_marburg/powersort/benchmark/JmhCompetition.java @@ -3,6 +3,7 @@ package de.uni_marburg.powersort.benchmark; import de.uni_marburg.powersort.data.CompetitionEnum; import de.uni_marburg.powersort.data.DataEnum; import de.uni_marburg.powersort.sort.SortEnum; +import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; @@ -28,4 +29,9 @@ public class JmhCompetition extends JmhBase { SortEnum getSortEnum(){ return sortEnum; } + + @Benchmark + public void benchmark() { + sortImpl.sort(workingCopy); + } } diff --git a/intellij-jmh.png b/intellij-jmh.png new file mode 100644 index 0000000000000000000000000000000000000000..68ffbf4e4117e442a1ef7e42ebd3cd477488664c GIT binary patch literal 14199 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