1
0

Complete runnable Vector API (JEP 537) demo

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.
This commit was merged in pull request #1.
This commit is contained in:
2026-07-26 11:50:15 +00:00
parent e4695ab334
commit 780848e102
18 changed files with 824 additions and 2 deletions

123
RESULTS.md Normal file
View File

@@ -0,0 +1,123 @@
# Captured results
Everything below was produced by the code in this repository. Your numbers will
differ — that is the point of shipping the code rather than only the table.
## Environment
```
JDK : OpenJDK 21.0.2+13-58 (HotSpot 64-Bit Server VM, mixed mode)
OS : Ubuntu 22.04.5 LTS, Linux 6.8.0-124-generic, x86_64
CPU : AMD Ryzen 5 5600U (Zen 3), 2 vCPUs (1 core / 2 threads) exposed
Hypervisor : Microsoft Hyper-V, full virtualization
ISA : AVX2 + FMA; no AVX-512 -> FloatVector.SPECIES_PREFERRED = 8 lanes, 256-bit
Caches : L1d 32 KiB, L2 512 KiB, L3 16 MiB (as reported to the guest)
JVM flags : --add-modules jdk.incubator.vector (plus -XX:-UseSuperWord where noted)
Threads : single-threaded throughout
Data : 8192 floats = 32 KiB per array, cache-resident
Frequency : not controllable inside the guest (no cpufreq governor exposed)
```
The last line matters: on a virtualized, frequency-scaled machine, absolute
nanoseconds are soft. Ratios measured back-to-back in the same JVM are far more
stable than the absolute numbers, and the JMH table is more trustworthy than the
exploratory one.
## 1. Exploratory timings — `timings/run.sh`
Harness: 50,000 warmup calls, then 15 rounds of 5,000 calls; median reported.
These are teaching experiments, not statistically rigorous benchmarks.
```
--- kernel 1: c[i] = -(a[i]^2 + b[i]^2), 8192 floats ---
scalar for-loop median 832.0 ns/call best 819.6 ns/call
Vector API median 1279.9 ns/call best 1207.8 ns/call
vector advantage 0.65x
--- kernel 2: masked filter-and-sum, 8192 floats ---
scalar sum = 3084.1404
vector sum = 3084.1448 (same value, different rounding)
scalar (branch) median 5021.9 ns/call best 4926.9 ns/call
Vector API (masked) median 1029.9 ns/call best 1021.4 ns/call
vector advantage 4.88x
--- kernel 3: image brightness + clamp, 8192 pixels ---
scalar (branchy clamp) median 6881.6 ns/call best 6839.2 ns/call
Vector API (max/min) median 1243.8 ns/call best 1222.6 ns/call
vector advantage 5.53x
--- floating-point reduction ordering ---
scalar (left to right) : 4103.688965
vector (lanes, then reduce): 4103.681641
```
## 2. Same experiments with auto-vectorization off — `timings/run.sh -XX:-UseSuperWord`
```
--- kernel 1 --- scalar 3427.0 ns/call vector 1230.1 ns/call -> 2.79x
--- kernel 2 --- scalar 5040.4 ns/call vector 1031.3 ns/call -> 4.89x
--- kernel 3 --- scalar 6883.9 ns/call vector 1364.1 ns/call -> 5.05x
```
Read the two runs together — that comparison is the whole experiment:
| Scalar loop | SuperWord ON | SuperWord OFF | Conclusion |
|---|---|---|---|
| kernel 1 (straight-line math) | 832 ns | 3427 ns | C2 **was** vectorizing it; disabling SuperWord costs 4.1x |
| kernel 2 (data-dependent branch) | 5022 ns | 5040 ns | unchanged, so C2 was **not** vectorizing it |
| kernel 3 (branchy clamp) | 6882 ns | 6884 ns | unchanged, so C2 was **not** vectorizing it |
The Vector API versions are essentially unmoved by the flag in all three cases,
which is the property the API is actually selling.
Run-to-run spread on this machine was a few percent for kernels 1 and 2, and up
to ~13% for kernel 3 (5.05x6.28x observed across runs) — another reminder to
quote JMH rather than a stopwatch.
## 3. JMH — `jmh/run.sh`
JMH 1.37, 1 fork, 3 warmup iterations x 1 s, 5 measurement iterations x 1 s,
single thread, average time per operation.
```
Benchmark Mode Cnt Score Error Units
VectorBench.scalar avgt 5 838.631 ± 106.657 ns/op
VectorBench.vector avgt 5 814.531 ± 10.766 ns/op
VectorBench.vectorChecksum avgt 5 7409.993 ± 224.753 ns/op
MaskedBench.scalarBranch avgt 5 5189.874 ± 250.597 ns/op
MaskedBench.vectorMasked avgt 5 1036.419 ± 27.726 ns/op
BrightnessBench.scalarClamp avgt 5 6510.091 ± 302.638 ns/op
BrightnessBench.vectorClamp avgt 5 1133.073 ± 17.763 ns/op
```
Three readings:
1. **Square-and-add: no measurable difference.** 838.6 ± 106.7 vs 814.5 ± 10.8 —
the error bars overlap. On a loop C2 already vectorizes, hand-writing the
vector loop bought nothing measurable here. (The exploratory harness made the
vector version look ~35% slower; that gap is harness overhead, not the kernel.
This is exactly why the JMH number is the one to quote.)
2. **Masked filter: 5.0x** (5189.9 / 1036.4), with tight error bars on the vector side.
3. **Brightness clamp: 5.7x** (6510.1 / 1133.1).
`vectorChecksum` is a deliberate cautionary example: returning a checksum does
prevent dead-code elimination, but the checksum loop is then *inside* the
measurement, and the benchmark reports ~9x the time of the kernel it was meant to
measure. Consuming the output array through a `Blackhole` keeps the measurement
honest without adding work.
## What is not reproduced here
The article shows an excerpt of the AVX2 machine code C2 emits for the
square-and-add loop. Reproducing that needs an `hsdis` disassembler plugin
installed next to your JDK:
```
java --add-modules jdk.incubator.vector \
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly \
-XX:CompileCommand=print,com.ankurm.vectorapi.SquareAdd::vectorComputation \
-cp out com.ankurm.vectorapi.Main
```
Without `hsdis` the JVM prints `Loading hsdis library failed` and falls back to a
hex dump.