Core Java

Why the Java Vector API Is on Its 12th Incubation (JEP 537) and What It Is Really Waiting For

Twelve rounds of incubation is a strange record for an API that, by all accounts, works. So what is the delay? The usual answer is “Valhalla”, but that raises a sharper question: if auto-vectorization already handles many loops, who needs this API at all? To find out, I measured both on JDK 27, and the results are more nuanced than either camp expects.

Twelve Rounds, Almost No Change

JEP 537 proposes to re-incubate the Vector API in JDK 27 without substantial change. The history it lists runs from JEP 338 in JDK 16 through JEP 529 in JDK 26. In other words, this is the twelfth consecutive release carrying the same incubating API. According to InfoWorld’s JDK 27 review, the repeat is not because the API has significant issues.

The history section of JEP 537 (JEP numbers 338, 414, 417, 426, 438, 448, 460, 469, 489, 508, 529, 537).

What It Is Waiting For

The JEP states the condition plainly. The API will keep incubating until the necessary Project Valhalla features are available as preview features. Then the team will adapt the API and promote it from incubation to preview. Meanwhile, JEP 529 explains the main change ahead: turning the API’s current value-based classes into real value classes.

Why does that matter? Today a FloatVector is an ordinary heap object with an identity. Therefore, the compiler has to prove that these temporary objects never need to exist, so it can keep the data in a CPU register. That is my summary, not the JEP’s wording, but it explains why the API wants identity-free objects.

PieceStatusSource
JEP 537, Vector APITargeted to JDK 27, no substantial changesInfoQ
JEP 401, value objectsPreview in JDK 28 (planned March 2027)The Register
JEP 539, strict field initializationCompanion JEP, also in JDK 28InfoWorld
JEP 401 leaving previewBrian Goetz called hoping for JDK 29 “optimistic”The Register

A Real SIMD Benchmark

Now for the measurement. I tested three kernels on 16,384-element arrays: an elementwise float map, an integer sum, and a float dot product. For each, I compared a plain Java loop (which C2 may auto-vectorize) with a Vector API version. The machine was a shared single-core VM with an Intel Xeon at 2.1 GHz and AVX-512, running Temurin 27.0.1. Each number is the median of five fresh JVMs, each with a 3-second warm-up and 15 timed samples.

You can try the dot-product case yourself. It needs JDK 21 or newer, and the compiler prints an incubator warning, which is normal. I ran this exact block on JDK 27 and JDK 21.

cat > Dot.java <<'EOF'
import jdk.incubator.vector.*;
import java.util.Arrays;

public class Dot {
    static final VectorSpecies<Float> S = FloatVector.SPECIES_PREFERRED;
    static final int N = 1 << 14;

    static float scalar(float[] a, float[] b) {
        float s = 0;
        for (int i = 0; i < N; i++) s += a[i] * b[i];
        return s;
    }

    static float vector(float[] a, float[] b) {
        var acc = FloatVector.zero(S);
        for (int i = 0; i < N; i += S.length())
            acc = FloatVector.fromArray(S, a, i)
                             .fma(FloatVector.fromArray(S, b, i), acc);
        return acc.reduceLanes(VectorOperators.ADD);
    }

    static double micros(boolean vec, float[] a, float[] b) {
        float sink = 0;
        long end = System.nanoTime() + 2_000_000_000L;   // 2 s warm-up
        while (System.nanoTime() < end) sink += vec ? vector(a, b) : scalar(a, b);
        double[] t = new double[11];
        for (int j = 0; j < t.length; j++) {
            long t0 = System.nanoTime();
            for (int n = 0; n < 1000; n++) sink += vec ? vector(a, b) : scalar(a, b);
            t[j] = (System.nanoTime() - t0) / 1e6;       // microseconds per call
        }
        Arrays.sort(t);
        if (sink == 42) System.out.println();
        return t[t.length / 2];                          // median
    }

    public static void main(String[] args) {
        float[] a = new float[N], b = new float[N];
        for (int i = 0; i < N; i++) { a[i] = i % 7; b[i] = i % 5; }
        System.out.println("lanes: " + S.length());
        System.out.printf("scalar: %.1f us%n", micros(false, a, b));
        System.out.printf("vector: %.1f us%n", micros(true, a, b));
    }
}
EOF

java --add-modules jdk.incubator.vector Dot.java

On my machine, repeated runs on JDK 27 printed 13.6 to 18.8 microseconds for scalar and about 1.5 for vector. Your numbers will differ with your CPU, and a shared machine adds noise. Note that the vector version sums in a different order, so its result is not bit-identical to the scalar one.

KernelVariantMedian per call (ns)Per element (ns)Range, 5 runs (ns)
Float mapPlain loop1,7960.1101,670 to 2,388
Float mapVector API2,3620.1442,003 to 2,487
Int sumPlain loop4150.025343 to 443
Int sumVector API6880.042624 to 705
Float dotPlain loop18,5521.13214,038 to 18,791
Float dotHand-unrolled, 8 accumulators4,2970.2623,743 to 4,375
Float dotVector API1,5010.0921,497 to 1,579

Own measurement, hand-written harness (not JMH), shared VM, so treat ratios as indicative. Assembly was not inspected

Ratios of the medians in the table above. Own measurement on JDK 27.0.1, AVX-512 Xeon, single shared vCPU.

What the Numbers Say

First, the compiler held its own. On the elementwise map and the integer sum, the plain loop beat my Vector API code. I cannot confirm the loops were vectorized without reading the assembly, but they ran far faster than a scalar loop would. Second, the same integer-sum loop took roughly 4,000 to 5,200 ns on JDK 21 in my three trial runs, so auto-vectorization has clearly improved between releases.

However, the float dot product tells a different story. Java float addition is not associative, so the compiler is not free to reorder a running sum. As a result, the plain loop was about 12 times slower than the Vector API, and even eight hand-written accumulators were nearly 3 times slower. The Vector API lets you state explicitly that you accept a different summation order.

The Provocation

Here is the uncomfortable part. If auto-vectorization keeps improving, the Vector API’s real audience shrinks to loops where the language forbids the compiler from helping, such as float reductions, plus code needing masks, shuffles or explicit lane control. That niche is real, but it is narrow, and it makes the wait tolerable. Still, there is a cost: an incubator module needs --add-modules on every run, which keeps it away from many libraries.

Checklist: should you reach for the Vector API?

  • You measured a plain loop first and it is the bottleneck.
  • The loop involves a float or double reduction, or lane-level control.
  • You can accept a changed summation order and a different last digit.
  • You can add the incubator module flag to your build and launch scripts.

What We Learned

JEP 537 is the twelfth incubation of the Vector API, and it comes with no substantial changes because the team is waiting for Project Valhalla’s value classes, which arrive as a preview in JDK 28. Our benchmark on JDK 27 showed that the compiler already matched or beat the Vector API on an elementwise map and an integer sum. By contrast, the API was about 12 times faster than a plain float dot-product loop, because strict floating-point ordering blocks auto-vectorization. So the API is not obsolete, but its value sits in a narrow set of cases. Measure your loop first, and reach for the Vector API only where the compiler cannot follow.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button