Fundamentals 8 min read

Using JMH to Benchmark GUID Generation Strategies in Java

This article introduces JMH, explains its key features, and presents microbenchmark results comparing thread‑exclusive, thread‑shared, Snowflake, UUID, and Snowflake‑algorithm GUID generation methods under various thread counts, accompanied by the full Java test code.

FunTester
FunTester
FunTester
Using JMH to Benchmark GUID Generation Strategies in Java

In a previous post I discussed several common ways to generate globally unique identifiers (GUIDs) for performance testing, but without real test data the conclusions were speculative. To obtain concrete measurements I turned to the Java Microbenchmark Harness (JMH), a widely used tool for writing and running Java microbenchmarks.

JMH Overview

JMH provides high‑confidence results by eliminating noise and bias, offers a rich set of annotations and APIs for ease of use, supports multiple benchmark modes (throughput, latency, etc.), allows custom environment settings such as GC strategies, and is extensively used throughout the Java ecosystem, including the JDK itself.

Experimental Setup

Four GUID generation schemes (excluding a distributed service‑based approach) were benchmarked: thread‑exclusive counter, thread‑shared atomic counter, Snowflake algorithm, and Java UUID.randomUUID() . Each benchmark was warmed up twice with a batch size of 2, then measured once with a batch size of 1. The JMH configuration used was:

.warmupIterations(2) // pre‑warm count
.warmupBatchSize(2) // pre‑warm batch size
.measurementIterations(1) // measurement count
.measurementBatchSize(1) // measurement batch size
.build();

Results were collected for 1, 10, and 40 concurrent threads on a 12‑core machine. The throughput (operations per microsecond) showed that both the thread‑exclusive and thread‑shared counters far outperformed UUID and the Snowflake algorithm.

Single‑Thread Results

UniqueNumberTest.exclusive  thrpt  203.146  ops/us
UniqueNumberTest.share      thrpt   99.860  ops/us
UniqueNumberTest.snow       thrpt    4.096  ops/us
UniqueNumberTest.uuid       thrpt   11.758  ops/us

10‑Thread Results

Benchmark                     Mode  Cnt    Score   Error   Units
UniqueNumberTest.exclusive   thrpt 1117.347          ops/us
UniqueNumberTest.share        thrpt  670.141          ops/us
UniqueNumberTest.snow         thrpt   10.925          ops/us
UniqueNumberTest.uuid         thrpt    3.608          ops/us

40‑Thread Results

Benchmark                     Mode  Cnt    Score   Error   Units
UniqueNumberTest.exclusive   thrpt 1110.273          ops/us
UniqueNumberTest.share        thrpt  649.350          ops/us
UniqueNumberTest.snow         thrpt    8.908          ops/us
UniqueNumberTest.uuid         thrpt    4.205          ops/us

The performance scales similarly from 10 to 40 threads, indicating the CPU was near full utilization. Multiplying the reported microseconds by one million yields the per‑second operation count, which can guide selection of a GUID generation strategy.

Test Case Code

package com.funtest.jmh;

import com.funtester.utils.SnowflakeUtils;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
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.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class UniqueNumberTest {

    SnowflakeUtils snowflakeUtils = new SnowflakeUtils(1, 1);
    ThreadLocal
exclusive = ThreadLocal.withInitial(() -> 0);
    AtomicInteger share = new AtomicInteger(0);

    @Benchmark
    public void uuid() {
        UUID.randomUUID();
    }

    @Benchmark
    public void snow() {
        snowflakeUtils.nextId();
    }

    @Benchmark
    public void exclusive(Blackhole blackhole) {
        Integer i = exclusive.get();
        i++;
        blackhole.consume(i + "");
    }

    @Benchmark
    public void share(Blackhole blackhole) {
        blackhole.consume(share.incrementAndGet() + "");
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(UniqueNumberTest.class.getSimpleName())
                .result("long/result.json")
                .resultFormat(ResultFormatType.JSON)
                .forks(1)
                .threads(40)
                .warmupIterations(2)
                .warmupBatchSize(2)
                .measurementIterations(1)
                .measurementBatchSize(1)
                .build();
        new Runner(options).run();
    }
}

Note: JMH currently does not support Groovy, so the benchmark was written in Java.

Javaperformance testingBenchmarkingMicrobenchmarkJMHGUID
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.