Skip to content

Instantly share code, notes, and snippets.

@tivrfoa
Last active February 28, 2025 19:42
Show Gist options
  • Save tivrfoa/e8f383f3010d0476f821905f27c8e716 to your computer and use it in GitHub Desktop.
Save tivrfoa/e8f383f3010d0476f821905f27c8e716 to your computer and use it in GitHub Desktop.
Java benchmark program using JMH (Java Microbenchmark Harness) to compare the performance of TreeSet and PriorityQueue.

Prompt: Write a Java benchmark program using JMH, that benchmarks TreeSet vs PriorityQueue.


Here is a complete Java benchmark program using JMH (Java Microbenchmark Harness) to compare the performance of TreeSet and PriorityQueue. The benchmark focuses on three key operations: adding elements, adding and removing the smallest element, and checking for element existence. Benchmark Overview We will measure the performance of TreeSet and PriorityQueue for the following operations:

Adding Elements:

  • TreeSet.add(): O(log n) per insertion.
  • PriorityQueue.offer(): O(log n) per insertion. Adding and Removing the Smallest Element:
  • Add N elements and then remove the smallest element N times.
  • TreeSet.pollFirst(): O(log n) per removal.
  • PriorityQueue.poll(): O(log n) per removal. Checking for Element Existence:
  • TreeSet.contains(): O(log n) per check.
  • PriorityQueue.contains(): O(n) per check (linear search).

The benchmark uses JMH annotations to configure the execution, including multiple input sizes (N = 1000, 10000, 100000) to observe scaling behavior.

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.*;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(1)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
public class TreeSetVsPriorityQueueBenchmark {

    /** Size of the data structure, parameterized to test multiple values */
    @Param({"1000", "10000", "100000"})
    public int N;

    /** List of unique elements to add to the data structures */
    public List<Integer> elementsToAdd;

    /** List of elements to check for existence (mix of present and absent) */
    public List<Integer> elementsToCheck;

    /**
     * Setup method to prepare the input data before each trial.
     * Runs once per fork for each value of N.
     */
    @Setup(Level.Trial)
    public void setup() {
        // Generate N unique integers (0 to N-1) and shuffle them
        elementsToAdd = IntStream.range(0, N)
                                .boxed()
                                .collect(Collectors.toList());
        Collections.shuffle(elementsToAdd);

        // Generate N elements to check: ~half present, ~half absent
        List<Integer> present = new ArrayList<>(elementsToAdd); // Copy of elementsToAdd
        List<Integer> absent = IntStream.range(N, 2 * N)
                                       .boxed()
                                       .collect(Collectors.toList());
        elementsToCheck = new ArrayList<>();
        Random rand = new Random(0); // Fixed seed for reproducibility
        for (int i = 0; i < N; i++) {
            if (rand.nextBoolean()) {
                elementsToCheck.add(present.get(i % present.size()));
            } else {
                elementsToCheck.add(absent.get(i % absent.size()));
            }
        }
    }

    /** Benchmark adding elements to TreeSet */
    @Benchmark
    public void treeSetAdd(Blackhole blackhole) {
        TreeSet<Integer> set = new TreeSet<>();
        for (Integer e : elementsToAdd) {
            set.add(e);
        }
        blackhole.consume(set); // Prevent dead code elimination
    }

    /** Benchmark adding elements to PriorityQueue */
    @Benchmark
    public void priorityQueueOffer(Blackhole blackhole) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (Integer e : elementsToAdd) {
            pq.offer(e);
        }
        blackhole.consume(pq);
    }

    /** Benchmark adding and removing smallest elements from TreeSet */
    @Benchmark
    public void treeSetAddAndRemove(Blackhole blackhole) {
        TreeSet<Integer> set = new TreeSet<>();
        for (Integer e : elementsToAdd) {
            set.add(e);
        }
        while (!set.isEmpty()) {
            blackhole.consume(set.pollFirst());
        }
    }

    /** Benchmark adding and removing smallest elements from PriorityQueue */
    @Benchmark
    public void priorityQueueOfferAndPoll(Blackhole blackhole) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (Integer e : elementsToAdd) {
            pq.offer(e);
        }
        while (!pq.isEmpty()) {
            blackhole.consume(pq.poll());
        }
    }

    /** Benchmark checking element existence in TreeSet */
    @Benchmark
    public void treeSetContains(Blackhole blackhole) {
        TreeSet<Integer> set = new TreeSet<>();
        for (Integer e : elementsToAdd) {
            set.add(e);
        }
        for (Integer e : elementsToCheck) {
            blackhole.consume(set.contains(e));
        }
    }

    /** Benchmark checking element existence in PriorityQueue */
    @Benchmark
    public void priorityQueueContains(Blackhole blackhole) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (Integer e : elementsToAdd) {
            pq.offer(e);
        }
        for (Integer e : elementsToCheck) {
            blackhole.consume(pq.contains(e));
        }
    }

    /** Main method to run the benchmark (optional for manual execution) */
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

Explanation of Key Components Annotations

  • @State(Scope.Benchmark): Defines a shared state across all benchmark threads for a given trial, holding the input data.
  • @BenchmarkMode(Mode.AverageTime): Measures the average execution time of each operation.
  • @OutputTimeUnit(TimeUnit.MILLISECONDS): Reports results in milliseconds.
  • @Fork(1): Runs the benchmark in one JVM fork to isolate results.
  • @Warmup and @Measurement: Configures 5 warmup iterations and 5 measurement iterations, each lasting 1 second, to stabilize and measure performance.

State Setup

  • N: Parameterized with @Param to test different sizes (1000, 10000, 100000).
  • elementsToAdd: A shuffled list of N unique integers (0 to N-1) ensures random insertion order. Using unique elements makes the comparison fair, as TreeSet rejects duplicates while PriorityQueue allows them.
  • elementsToCheck: A list of N integers, approximately half present (from elementsToAdd) and half absent (from N to 2N-1), generated with a fixed-seed Random for reproducibility.

Benchmark Methods Each method follows a similar pattern:

  1. Create a new instance of the data structure (TreeSet or PriorityQueue). 2.Perform the operation(s) being benchmarked.
  2. Use Blackhole.consume() to prevent the JVM from optimizing away the computation.

Adding Elements:

  • treeSetAdd: Adds N elements to a TreeSet.
  • priorityQueueOffer: Adds N elements to a PriorityQueue. Adding and Removing:
  • treeSetAddAndRemove: Adds N elements, then removes the smallest element N times.
  • priorityQueueOfferAndPoll: Same, but for PriorityQueue. Contains Checks:
  • treeSetContains: Adds N elements, then checks N elements for existence.
  • priorityQueueContains: Same, but for PriorityQueue.

Running the Benchmark To run the benchmark, you need the JMH library. If you're using Maven, add the following dependencies to your pom.xml:

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

Run via Maven:

    mvn clean install
    java -jar target/benchmarks.jar

Expected Results

  • Adding Elements: Both TreeSet and PriorityQueue should perform similarly (O(N log N) total time), though PriorityQueue (binary heap) may have a lower constant factor than TreeSet (red-black tree) due to simpler balancing.
  • Adding and Removing: Similar performance is expected (O(N log N) total time), with PriorityQueue potentially faster due to heap operations being lighter than tree rebalancing.
  • Contains Checks: TreeSet should significantly outperform PriorityQueue (O(N log N) vs. O(N²) total time) because PriorityQueue uses a linear search for contains().

These results will help you choose between TreeSet and PriorityQueue based on your application's needs (e.g., fast membership checks vs. priority-based removal).


This benchmark provides a comprehensive comparison of TreeSet and PriorityQueue for common operations, suitable for evaluating their performance in various scenarios.

@tivrfoa
Copy link
Author

tivrfoa commented Feb 28, 2025

Prompt:
show me the complete pom.xml to run this benchmark


To run a JMH (Java Microbenchmark Harness) benchmark, you need a properly configured pom.xml file that includes the necessary dependencies and plugins. Below is a complete pom.xml file tailored for running a JMH benchmark, such as one comparing TreeSet and PriorityQueue. This configuration ensures that the benchmark is compiled with annotation processing and packaged into an executable JAR for easy execution.
Complete pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>jmh-benchmark</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.openjdk.jmh</groupId>
                            <artifactId>jmh-generator-annprocess</artifactId>
                            <version>1.36</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>benchmarks</finalName>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Explanation of the Configuration

Project Declaration
    groupId: com.example - A unique identifier for your project.
    artifactId: jmh-benchmark - The name of your project.
    version: 1.0-SNAPSHOT - The version of your project, indicating it’s under development.
Properties
    maven.compiler.source: 1.8 - Specifies the Java source version as 8.
    maven.compiler.target: 1.8 - Ensures the compiled bytecode is compatible with Java 8, which is widely supported by JMH.
Dependencies
    jmh-core
        groupId: org.openjdk.jmh
        artifactId: jmh-core
        version: 1.36 - The core JMH library required to run benchmarks.
    jmh-generator-annprocess
        groupId: org.openjdk.jmh
        artifactId: jmh-generator-annprocess
        version: 1.36
        scope: provided - This dependency is used for annotation processing during compilation and is not needed at runtime.
Build Plugins
    maven-compiler-plugin
        version: 3.8.1 - A stable version compatible with Java 8.
        configuration: Specifies jmh-generator-annprocess as an annotation processor to process JMH annotations (e.g., @Benchmark, @Param) during compilation.
    maven-shade-plugin
        version: 3.2.4 - A reliable version for creating shaded JARs.
        execution: Runs during the package phase to create a standalone JAR.
        configuration:
            finalName: benchmarks - Names the output JAR as benchmarks.jar.
            transformers: Sets the main class to org.openjdk.jmh.Main, allowing the JAR to run all benchmarks directly.

How to Use This pom.xml

Save the File
    Place this pom.xml in the root directory of your Maven project, alongside your benchmark source code (e.g., in src/main/java).
Build the Project
    Open a terminal in the project directory and run:
    bash

    mvn clean package

    This command compiles the code, processes JMH annotations, and packages everything into target/benchmarks.jar.
Run the Benchmark
    Execute the generated JAR with:
    bash

    java -jar target/benchmarks.jar

    This launches the JMH runner, which executes all benchmarks defined in your source code and outputs the performance results.

Notes

Ensure your benchmark class (e.g., MyBenchmark.java) is in the correct package under src/main/java (e.g., src/main/java/com/example/).
The JMH version 1.36 is used for consistency, but you can check the [Maven Central Repository](https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core) for the latest version if desired.
The shaded JAR includes all dependencies, making it portable and easy to run on any system with Java 8 or higher installed.

This pom.xml provides a robust setup for running JMH benchmarks, handling both compilation and execution seamlessly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment