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

9
timings/run.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Compile and run the exploratory timing experiments from the article.
# ./run.sh -> default run (C2 auto-vectorization ON)
# ./run.sh -XX:-UseSuperWord -> same experiments with SuperWord disabled
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p out
javac --add-modules jdk.incubator.vector -d out $(find src -name '*.java')
java --add-modules jdk.incubator.vector "$@" -cp out com.ankurm.vectorapi.Main

View File

@@ -0,0 +1,41 @@
package com.ankurm.vectorapi;
import java.util.Arrays;
/**
* A deliberately small timing harness. It is NOT a statistical benchmark:
* it warms the JIT up, then reports the median and best of several rounds so
* that large effects (2x and up) are visible. Use JMH for anything you intend
* to defend -- see the jmh/ module in this repository.
*/
public final class Bench {
public static final int WARMUP_CALLS = 50_000;
public static final int ROUND_CALLS = 5_000;
public static final int ROUNDS = 15;
public interface Kernel { void run(); }
/** @return median ns/call over ROUNDS rounds (also prints best). */
public static double measure(String label, Kernel k) {
for (int i = 0; i < WARMUP_CALLS; i++) k.run(); // force C2 compilation
double[] perCall = new double[ROUNDS];
for (int r = 0; r < ROUNDS; r++) {
long t0 = System.nanoTime();
for (int i = 0; i < ROUND_CALLS; i++) k.run();
long t1 = System.nanoTime();
perCall[r] = (t1 - t0) / (double) ROUND_CALLS;
}
double[] sorted = perCall.clone();
Arrays.sort(sorted);
double median = sorted[ROUNDS / 2];
double best = sorted[0];
System.out.printf("%-24s median %8.1f ns/call best %8.1f ns/call%n", label, median, best);
return median;
}
public static void speedup(String label, double scalarNs, double vectorNs) {
System.out.printf("%-24s %.2fx%n", label, scalarNs / vectorNs);
}
private Bench() {}
}

View File

@@ -0,0 +1,51 @@
package com.ankurm.vectorapi;
import jdk.incubator.vector.*;
/** Image brightness: scale, bias, clamp to [0, 255]. The clamp is two branches. */
public final class Brightness {
static final VectorSpecies<Float> SP = FloatVector.SPECIES_PREFERRED;
// Scalar: the clamp is two data-dependent branches
static void brightScalar(float[] in, float[] out, float gain, float bias) {
for (int i = 0; i < in.length; i++) {
float v = in[i] * gain + bias;
if (v < 0f) v = 0f; else if (v > 255f) v = 255f;
out[i] = v;
}
}
static void brightVector(float[] in, float[] out, float gain, float bias) {
var vg = FloatVector.broadcast(SP, gain);
var vb = FloatVector.broadcast(SP, bias);
var lo = FloatVector.zero(SP);
var hi = FloatVector.broadcast(SP, 255f);
int i = 0, bound = SP.loopBound(in.length);
for (; i < bound; i += SP.length()) {
FloatVector.fromArray(SP, in, i).fma(vg, vb).max(lo).min(hi).intoArray(out, i);
}
for (; i < in.length; i++) { // scalar tail
float v = in[i] * gain + bias;
if (v < 0f) v = 0f; else if (v > 255f) v = 255f;
out[i] = v;
}
}
static void run(int n) {
float[] in = Data.randomFloats(n, 4);
for (int i = 0; i < n; i++) in[i] = in[i] * 300f - 20f; // some pixels clamp at both ends
float[] o1 = new float[n], o2 = new float[n];
brightScalar(in, o1, 1.4f, 12f);
brightVector(in, o2, 1.4f, 12f);
Data.assertSame("brightness", o1, o2);
System.out.println("--- kernel 3: image brightness + clamp, " + n + " pixels ---");
double s = Bench.measure("scalar (branchy clamp)", () -> brightScalar(in, o1, 1.4f, 12f));
double v = Bench.measure("Vector API (max/min)", () -> brightVector(in, o2, 1.4f, 12f));
Bench.speedup("vector advantage", s, v);
System.out.println();
}
private Brightness() {}
}

View File

@@ -0,0 +1,22 @@
package com.ankurm.vectorapi;
import java.util.Random;
public final class Data {
static float[] randomFloats(int n, long seed) {
Random r = new Random(seed);
float[] a = new float[n];
for (int i = 0; i < n; i++) a[i] = r.nextFloat();
return a;
}
static void assertSame(String what, float[] x, float[] y) {
for (int i = 0; i < x.length; i++) {
if (Math.abs(x[i] - y[i]) > 1e-4f) {
throw new AssertionError(what + ": mismatch at " + i + ": " + x[i] + " != " + y[i]);
}
}
}
private Data() {}
}

View File

@@ -0,0 +1,24 @@
package com.ankurm.vectorapi;
import jdk.incubator.vector.*;
/** Prints the exact environment a benchmark run happened on. */
public final class Env {
public static void print() {
Runtime rt = Runtime.getRuntime();
System.out.println("=== environment ===");
System.out.println("JDK : " + System.getProperty("java.vm.name")
+ " " + System.getProperty("java.vm.version"));
System.out.println("java.version : " + System.getProperty("java.version"));
System.out.println("OS : " + System.getProperty("os.name") + " "
+ System.getProperty("os.version") + " (" + System.getProperty("os.arch") + ")");
System.out.println("Available procs: " + rt.availableProcessors());
System.out.println("Preferred spec : " + FloatVector.SPECIES_PREFERRED
+ " (" + FloatVector.SPECIES_PREFERRED.length() + " float lanes, "
+ FloatVector.SPECIES_PREFERRED.vectorBitSize() + "-bit)");
System.out.println("Max vector size: " + IntVector.SPECIES_PREFERRED.vectorBitSize() + "-bit (int)");
System.out.println();
}
private Env() {}
}

View File

@@ -0,0 +1,25 @@
package com.ankurm.vectorapi;
/**
* Runs every timing experiment quoted in the article, on whatever machine you
* are sitting at. These are exploratory timings, not statistically rigorous
* benchmarks -- see the jmh/ module for the harness you would defend numbers with.
*
* javac --add-modules jdk.incubator.vector -d out $(find src -name '*.java')
* java --add-modules jdk.incubator.vector -cp out com.ankurm.vectorapi.Main
* java --add-modules jdk.incubator.vector -XX:-UseSuperWord -cp out com.ankurm.vectorapi.Main
*/
public final class Main {
public static void main(String[] args) {
int n = args.length > 0 ? Integer.parseInt(args[0]) : 8192;
Env.print();
System.out.println("array length : " + n + " floats (" + (n * 4 / 1024) + " KiB, cache-resident)");
System.out.println();
SquareAdd.run(n);
MaskedFilter.run(n);
Brightness.run(n);
ReductionOrder.run();
}
private Main() {}
}

View File

@@ -0,0 +1,45 @@
package com.ankurm.vectorapi;
import jdk.incubator.vector.*;
/** Sum only the elements above a threshold -- a data-dependent branch. */
public final class MaskedFilter {
static final float THRESH = 0.5f;
static final VectorSpecies<Float> SP = FloatVector.SPECIES_PREFERRED;
// Scalar: the branch is data-dependent, so C2 generally cannot vectorize this form
static float scalar(float[] a) {
float sum = 0;
for (int i = 0; i < a.length; i++)
if (a[i] > THRESH) sum += a[i];
return sum;
}
static float vector(float[] a) {
FloatVector acc = FloatVector.zero(SP);
int i = 0, bound = SP.loopBound(a.length);
for (; i < bound; i += SP.length()) {
var v = FloatVector.fromArray(SP, a, i);
VectorMask<Float> m = v.compare(VectorOperators.GT, THRESH);
acc = acc.add(v, m); // add only where mask is true
}
float sum = acc.reduceLanes(VectorOperators.ADD); // horizontal sum of the lanes
for (; i < a.length; i++) if (a[i] > THRESH) sum += a[i]; // masked-off tail
return sum;
}
static void run(int n) {
float[] a = Data.randomFloats(n, 3);
float s0 = scalar(a), v0 = vector(a);
System.out.println("--- kernel 2: masked filter-and-sum, " + n + " floats ---");
System.out.printf("scalar sum = %.4f%nvector sum = %.4f (same value, different rounding)%n", s0, v0);
double s = Bench.measure("scalar (branch)", () -> Sink.consume(scalar(a)));
double v = Bench.measure("Vector API (masked)", () -> Sink.consume(vector(a)));
Bench.speedup("vector advantage", s, v);
System.out.println();
}
private MaskedFilter() {}
}

View File

@@ -0,0 +1,35 @@
package com.ankurm.vectorapi;
import jdk.incubator.vector.*;
/** Why a vector reduction and a scalar loop disagree in the last bits. */
public final class ReductionOrder {
static final VectorSpecies<Float> SP = FloatVector.SPECIES_PREFERRED;
static float scalarSum(float[] a) {
float sum = 0;
for (float v : a) sum += v; // strictly left to right
return sum;
}
static float vectorSum(float[] a) {
FloatVector acc = FloatVector.zero(SP);
int i = 0, bound = SP.loopBound(a.length);
for (; i < bound; i += SP.length()) acc = acc.add(FloatVector.fromArray(SP, a, i));
float sum = acc.reduceLanes(VectorOperators.ADD); // lanes combined at the end
for (; i < a.length; i++) sum += a[i];
return sum;
}
static void run() {
float[] a = Data.randomFloats(8192, 5);
System.out.println("--- floating-point reduction ordering ---");
System.out.printf("scalar (left to right) : %.6f%n", scalarSum(a));
System.out.printf("vector (lanes, then reduce): %.6f%n", vectorSum(a));
System.out.println("Same mathematical sum; different addition order, so different rounding.");
System.out.println();
}
private ReductionOrder() {}
}

View File

@@ -0,0 +1,10 @@
package com.ankurm.vectorapi;
/** Keeps computed values observable so the JIT cannot delete the computation. */
public final class Sink {
public static volatile float sink;
public static void consume(float v) { sink = v; }
private Sink() {}
}

View File

@@ -0,0 +1,50 @@
package com.ankurm.vectorapi;
import jdk.incubator.vector.*;
/** c[i] = -(a[i]^2 + b[i]^2) -- the loop C2's SuperWord pass handles well. */
public final class SquareAdd {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
static void scalarComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = -(a[i] * a[i] + b[i] * b[i]);
}
}
static void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length); // e.g. 8192 -> 8192, 8190 -> 8184
// Main loop: process SPECIES.length() elements per iteration
for (; i < upperBound; i += SPECIES.length()) {
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
va.mul(va).add(vb.mul(vb)).neg().intoArray(c, i);
}
// Scalar tail: the last (a.length % laneCount) elements
for (; i < a.length; i++) {
c[i] = -(a[i] * a[i] + b[i] * b[i]);
}
}
static void run(int n) {
float[] a = Data.randomFloats(n, 1);
float[] b = Data.randomFloats(n, 2);
float[] c1 = new float[n], c2 = new float[n];
scalarComputation(a, b, c1);
vectorComputation(a, b, c2);
Data.assertSame("square-add", c1, c2);
System.out.println("--- kernel 1: c[i] = -(a[i]^2 + b[i]^2), " + n + " floats ---");
double s = Bench.measure("scalar for-loop", () -> scalarComputation(a, b, c1));
double v = Bench.measure("Vector API", () -> vectorComputation(a, b, c2));
Bench.speedup("vector advantage", s, v);
System.out.println();
}
private SquareAdd() {}
}