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:
9
timings/run.sh
Normal file
9
timings/run.sh
Normal 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
|
||||
41
timings/src/main/java/com/ankurm/vectorapi/Bench.java
Normal file
41
timings/src/main/java/com/ankurm/vectorapi/Bench.java
Normal 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() {}
|
||||
}
|
||||
51
timings/src/main/java/com/ankurm/vectorapi/Brightness.java
Normal file
51
timings/src/main/java/com/ankurm/vectorapi/Brightness.java
Normal 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() {}
|
||||
}
|
||||
22
timings/src/main/java/com/ankurm/vectorapi/Data.java
Normal file
22
timings/src/main/java/com/ankurm/vectorapi/Data.java
Normal 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() {}
|
||||
}
|
||||
24
timings/src/main/java/com/ankurm/vectorapi/Env.java
Normal file
24
timings/src/main/java/com/ankurm/vectorapi/Env.java
Normal 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() {}
|
||||
}
|
||||
25
timings/src/main/java/com/ankurm/vectorapi/Main.java
Normal file
25
timings/src/main/java/com/ankurm/vectorapi/Main.java
Normal 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() {}
|
||||
}
|
||||
45
timings/src/main/java/com/ankurm/vectorapi/MaskedFilter.java
Normal file
45
timings/src/main/java/com/ankurm/vectorapi/MaskedFilter.java
Normal 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() {}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
10
timings/src/main/java/com/ankurm/vectorapi/Sink.java
Normal file
10
timings/src/main/java/com/ankurm/vectorapi/Sink.java
Normal 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() {}
|
||||
}
|
||||
50
timings/src/main/java/com/ankurm/vectorapi/SquareAdd.java
Normal file
50
timings/src/main/java/com/ankurm/vectorapi/SquareAdd.java
Normal 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() {}
|
||||
}
|
||||
Reference in New Issue
Block a user