Eight benchmark/demo classes, a deterministic test suite, and 12 captured output files generated by scripts/run-all.sh on JDK 25.0.5 (Temurin) with JMH 1.37. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6Fj2JNt7dRev4pZPe
54 lines
3.2 KiB
Markdown
54 lines
3.2 KiB
Markdown
# jmh-basics
|
|
|
|
Companion code for the ankurm.com post *"JMH Tutorial: Writing Java Microbenchmarks That Don't Lie."*
|
|
Module in `java-core-examples`, the Java-core series. There is deliberately no `docs/` folder: the
|
|
deeper material lives in collapsible sections inside the post itself.
|
|
|
|
## Versions this was built and tested against
|
|
|
|
| Component | Version | Notes |
|
|
|---|---|---|
|
|
| JDK | 25.0.5+8 (Temurin, LTS) | Compiler blackholes were auto-detected on this JVM; see `output/02`. |
|
|
| JMH | 1.37 | Latest GA on Maven Central; released 3 Aug 2023. |
|
|
| JUnit Jupiter | 5.11.0 | |
|
|
| Maven | 3.9.11 | |
|
|
| Host | 2 vCPU shared sandbox | Read the shape of the numbers, not the third significant digit. |
|
|
|
|
## Quickstart
|
|
|
|
```bash
|
|
export JAVA_HOME=/path/to/jdk-25
|
|
mvn -q package -DskipTests # builds target/benchmarks.jar (shaded, runnable)
|
|
java -cp target/classes com.ankurm.jmhbasics.NaiveTimingDemo
|
|
java -jar target/benchmarks.jar DeadCodeBenchmark
|
|
java -jar target/benchmarks.jar AllocationBenchmark -prof gc
|
|
```
|
|
|
|
`scripts/run-all.sh` regenerates every file in `output/` (about 8 minutes on 2 vCPUs).
|
|
`scripts/run.sh <ClassName>` runs one demo class ad hoc.
|
|
|
|
## What's in here
|
|
|
|
| File | What it shows |
|
|
|---|---|
|
|
| `NaiveTimingDemo.java` | The `System.nanoTime()` loop: a discarded result reports 0.000 ns/op, and the kept version changes from round to round while the JIT warms up. |
|
|
| `DeadCodeBenchmark.java` | Empty baseline, discarded result, returned result, `Blackhole`, and constant folding: `Math.pow` of a constant folds, `Math.log` of a constant does not, and a `final` instance field IS folded like a constant (an early draft of the Javadoc claimed the opposite; the measurement and JMHSample_10 both disagreed). |
|
|
| `LifecycleDemo.java` | Counts how often every `@Setup`/`@TearDown` level fires, by running JMH with `forks(0)` in-process. |
|
|
| `ForkingBenchmark.java` | One shared call site, two implementations, run with `-f 0` and `-f 1` to show type-profile pollution. |
|
|
| `StateScopeBenchmark.java` | `Scope.Thread` vs `Scope.Benchmark` with two threads. |
|
|
| `AllocationBenchmark.java` | Escape analysis: the same `new Point(..)` allocates 0 or 24 bytes per call depending on whether it escapes. Use `-prof gc`. |
|
|
| `SumBenchmark.java` | `@Param` over three input sizes, `@Setup` outside the measured region. |
|
|
| `SortBenchmark.java` | Warmup trace (`-wi 10 -w 200ms`) and three benchmark modes in one run. |
|
|
| `JmhBasicsTest.java` | Pins the deterministic claims: lifecycle counts, generated benchmark list, benchmark bodies. No timing assertions. |
|
|
| `output/01-12` | Captured runs of all of the above. |
|
|
|
|
## Notes worth knowing before reading the post
|
|
|
|
- **Each `@BenchmarkMode` value is a separate benchmark entry.** `SortBenchmark.sortCopy` carries three modes, so the generated `META-INF/BenchmarkList` has three lines for it (21 entries for 19 methods; the test pins this).
|
|
- **Run `LifecycleDemo` from the shaded jar**, not from `target/classes`: JMH is not on the plain classpath.
|
|
- **Timing figures drift.** Several error margins in `output/` are wide on this shared host. The post draws conclusions only from differences far larger than the error bars.
|
|
|
|
## License
|
|
|
|
MIT - see the [repo-wide LICENSE](../LICENSE).
|