SIMD kernels (square-and-add, masked filter, brightness clamp, FP reduction ordering), an exploratory timing harness, JMH benchmarks with Maven and Maven-free builds, run scripts, README and the captured results with their exact environment.
118 lines
4.9 KiB
Markdown
118 lines
4.9 KiB
Markdown
# Java Vector API (JEP 537) — runnable demo
|
|
|
|
Companion code for **[Java Vector API (JEP 537): SIMD, Auto-Vectorization, Masks, and Real Benchmarks](https://ankurm.com/java-vector-api-jep-537-simd-guide/)** on ankurm.com.
|
|
Every code listing and every number in that article comes from this repository.
|
|
|
|
## What this is about
|
|
|
|
Your CPU has vector registers — 256 bits wide on AVX2, 512 on AVX-512, 128 on
|
|
Arm NEON — that hold several numbers side by side, in *lanes*. A single SIMD
|
|
instruction applies one operation to every lane at once. A plain Java `for` loop
|
|
touches one element per iteration, so most of that hardware sits idle.
|
|
|
|
Java gives you two ways to use it:
|
|
|
|
* **Auto-vectorization (free, implicit).** HotSpot's C2 compiler has a SuperWord
|
|
pass that fuses consecutive scalar iterations into vector ones. When it fires
|
|
you get SIMD for nothing. It fires only when C2 can prove the transform is safe
|
|
and profitable — no data-dependent branches, no aliasing it cannot rule out,
|
|
regular access patterns — and when it declines, it does so silently.
|
|
* **The Vector API (explicit).** `jdk.incubator.vector` lets you state the vector
|
|
intent yourself: load a lane-width slice, do lane-wise arithmetic, store it
|
|
back, and use *masks* instead of branches. It does not promise a fixed
|
|
instruction sequence on every CPU, but it gives far more predictable access to
|
|
SIMD than hoping C2 infers it.
|
|
|
|
The API is still incubating — JEP 537 is its twelfth incubation, targeted at JDK
|
|
27 — because it is waiting on Project Valhalla's value classes. The shape of the
|
|
API is stable; the package name will change when it is promoted.
|
|
|
|
This repo demonstrates both paths, and — more usefully — shows how to **tell
|
|
which one you are on**, by running the same kernels with `-XX:-UseSuperWord`.
|
|
|
|
## Layout
|
|
|
|
```
|
|
timings/ exploratory timing experiments (plain javac, no dependencies)
|
|
src/main/java/com/ankurm/vectorapi/
|
|
Main.java runs everything, prints the environment first
|
|
SquareAdd.java c[i] = -(a[i]^2 + b[i]^2) — the loop C2 vectorizes for free
|
|
MaskedFilter.java sum elements above a threshold — masks vs a branch
|
|
Brightness.java scale + bias + clamp — max/min vs two branches
|
|
ReductionOrder.java why vector and scalar sums differ in the last bits
|
|
Bench.java the (deliberately simple) timing harness
|
|
Env.java, Data.java, Sink.java
|
|
run.sh
|
|
|
|
jmh/ the same kernels under JMH — the numbers worth defending
|
|
src/main/java/com/ankurm/vectorapi/
|
|
VectorBench.java square-and-add, incl. a cautionary checksum variant
|
|
MaskedBench.java masked filter-and-sum
|
|
BrightnessBench.java brightness + clamp
|
|
pom.xml
|
|
run.sh Maven build + run
|
|
run-without-maven.sh same thing with curl + javac, if you have no Maven
|
|
|
|
RESULTS.md everything the article quotes, with the exact machine it came from
|
|
```
|
|
|
|
## Requirements
|
|
|
|
JDK 21 or newer (anything with `jdk.incubator.vector`). Maven only for `jmh/run.sh`.
|
|
The incubator module must be named explicitly at compile time *and* run time:
|
|
|
|
```bash
|
|
javac --add-modules jdk.incubator.vector ...
|
|
java --add-modules jdk.incubator.vector ...
|
|
```
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
# 1. exploratory timings, C2 auto-vectorization on (the default)
|
|
./timings/run.sh
|
|
|
|
# 2. the same experiments with SuperWord disabled — the interesting comparison
|
|
./timings/run.sh -XX:-UseSuperWord
|
|
|
|
# 3. JMH, all benchmarks
|
|
./jmh/run.sh
|
|
|
|
# ...or one class, with a short config
|
|
./jmh/run.sh MaskedBench -wi 3 -i 5 -w 1 -r 1 -f 1
|
|
```
|
|
|
|
`timings/run.sh` prints the JDK build, OS, CPU count and the preferred vector
|
|
species before it measures anything, so a pasted result always carries the
|
|
environment it came from.
|
|
|
|
## What to expect
|
|
|
|
On the AVX2 machine described in [RESULTS.md](RESULTS.md):
|
|
|
|
| Kernel | Scalar | Vector API | Ratio |
|
|
|---|---|---|---|
|
|
| square-and-add (JMH) | 838.6 ± 106.7 ns | 814.5 ± 10.8 ns | indistinguishable — C2 already vectorized it |
|
|
| masked filter-and-sum (JMH) | 5189.9 ± 250.6 ns | 1036.4 ± 27.7 ns | **5.0x** |
|
|
| brightness + clamp (JMH) | 6510.1 ± 302.6 ns | 1133.1 ± 17.8 ns | **5.7x** |
|
|
|
|
The pattern is the point: where C2 auto-vectorizes, explicit code buys nothing;
|
|
where a data-dependent branch stops it, masks are worth roughly 5x. Confirm which
|
|
case you are in with `-XX:-UseSuperWord` before rewriting anything — if the scalar
|
|
loop does not get slower with that flag, C2 was not vectorizing it in the first
|
|
place.
|
|
|
|
These ratios are specific to this JDK, CPU, data size and threshold
|
|
distribution. Run them on your own hardware before quoting them.
|
|
|
|
## Further reading
|
|
|
|
* [The article this repo belongs to](https://ankurm.com/java-vector-api-jep-537-simd-guide/)
|
|
* [JEP 537: Vector API (Twelfth Incubator)](https://openjdk.org/jeps/537)
|
|
* [`jdk.incubator.vector` package docs](https://docs.oracle.com/en/java/javase/21/docs/api/jdk.incubator.vector/jdk/incubator/vector/package-summary.html)
|
|
* [JMH](https://github.com/openjdk/jmh)
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|