Almost every Java program spends some of its time in a loop that does the same arithmetic to every element of an array — scaling audio samples, normalizing a feature vector, summing a column, comparing pixels. You write that loop one element at a time because that is how a for loop reads. Your CPU, however, has been able to do eight, sixteen, or even sixty-four of those operations in a single instruction for over two decades. The gap between the loop you wrote and the hardware you own is the subject of this post.
There are two ways to close that gap in Java. The first is free and invisible: the JIT compiler sometimes rewrites your scalar loop into a vector loop behind your back. The second is explicit and reliable: the Vector API lets you write the vector loop yourself, so it is fast whether or not the compiler would have helped. This guide starts from what a CPU vector even is — assuming you have never thought about it — and ends at reference-grade detail: masks, the tail problem, floating-point reduction hazards, and the API's road out of incubation. Every benchmark here was compiled and run on a live JDK 21; the numbers and output are real, not illustrative.
Status (July 2026): The Vector API is still an incubating feature. JEP 537 proposes its twelfth incubation and is targeted to JDK 27 (due September 2026). It has incubated continuously since JDK 16 (JEP 338) because it is deliberately waiting for Project Valhalla: once value classes arrive as a preview feature, the API will be re-based on them and promoted from incubator to preview. Everything below uses the module jdk.incubator.vector and must be compiled and run with --add-modules jdk.incubator.vector. The shape of the API is stable; the package name will change when it leaves incubation.
The problem: your CPU is idling one lane at a time
Start with the most ordinary loop imaginable. Given three arrays, compute c[i] = -(a[i]² + b[i]²) for every element:
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]);
}
}
Read literally, this does one multiply, then another, then an add, then a negate, then a store — and only then moves to the next index. Each iteration touches exactly one float. That is the “scalar” world: one instruction, one datum. It is easy to read and easy for the compiler to reason about, but it leaves most of the CPU's arithmetic hardware switched off. To understand what is being wasted, you have to look one level down, at the registers the CPU actually computes with.
What a CPU vector actually is
In everyday math a vector is just a list of numbers. Inside a CPU it is the same idea, made physical: a single wide register that holds several numbers side by side. A modern x86 core has 256-bit registers (the AVX2 feature) and often 512-bit ones (AVX-512); Arm cores have 128-bit NEON registers and scalable SVE ones. Because a float is 32 bits, a 256-bit register holds exactly eight of them:
A 256-bit register packs eight 32-bit floats into eight independent lanes. Original diagram; lane terminology per the Vector API (JEP 537).
Each number in the register sits in its own lane. The register in the diagram has eight lanes; a 512-bit register would have sixteen. Nothing about the loop's logic requires processing one lane at a time — it was only ever a limitation of how we wrote it. The hardware is waiting for us to fill all eight lanes at once. The instruction that does exactly that has a name.
SIMD: one instruction, many data
The feature is called SIMD — Single Instruction, Multiple Data. A SIMD add takes two vector registers, adds lane 0 to lane 0, lane 1 to lane 1, and so on across all lanes, and writes all the sums in one shot. Conceptually:
A single SIMD instruction adds every lane pair at once. Original diagram.
If the eight-lane version of our loop runs in roughly the same time as one scalar iteration, then processing eight elements per step could, in the ideal case, be about eight times faster. Real speedups are smaller — memory bandwidth, tail elements, and instruction latency all take a cut — but the principle is why SIMD is the cheapest parallelism available: it happens inside a single core, with no threads, no locks, and no coordination. The natural next question is whether Java gives you any of this automatically. It does — sometimes.
Auto-vectorization: the free lunch you may already be eating
HotSpot's optimizing JIT compiler, C2, contains an optimization called SuperWord (auto-vectorization) that tries to fuse consecutive scalar iterations into one vector iteration. When it fires, you get SIMD speed without touching your code. Our scalarComputation loop above is close to the ideal case for it: a simple counted loop, contiguous a[i]/b[i] accesses, straight-line arithmetic, no branches. So does it actually fire? Rather than guess, measure. Here is a warmed-up benchmark comparing the plain scalar loop against a hand-written Vector API version (which we will dissect in a moment):
The hand-written vector loop is not faster — it is very slightly slower. That is not a failure; it is the punchline. C2 already auto-vectorized the scalar loop, so both versions are running SIMD instructions and the explicit one just carries a little more ceremony. You can prove the scalar loop was genuinely vectorized by asking the JVM to switch SuperWord off with the diagnostic flag -XX:-UseSuperWord and running the identical benchmark:
With auto-vectorization disabled, the scalar loop collapses to roughly 4.4× slower, while the Vector API version is unmoved — it was always doing SIMD, on purpose. Two lessons fall out of this single pair of runs. First, a lot of Java numeric code is already vectorized and you never knew. Second, and more importantly: the scalar loop's speed is entirely at the compiler's discretion, while the Vector API's speed is yours to guarantee. That distinction is the whole reason the API exists, so it is worth understanding exactly when the compiler's discretion runs out.
Why auto-vectorization is not enough
SuperWord only transforms a loop when C2 can prove the transformation is both safe and profitable. That proof needs a lot to line up: the loop must be a simple counted loop with a predictable trip count; memory accesses must be regular and non-overlapping (the compiler has to rule out aliasing, where a and c might be the same array); the body must use supported primitive operations; and the estimated payoff must beat the overhead given alignment and trip count. Violate any one of these and C2 quietly keeps the scalar code.
Real loops violate them constantly. They contain data-dependent branches (“only process elements above a threshold”), indirect indexing (a[index[i]]), reductions with awkward ordering constraints, mixed types, or calls the compiler cannot see through. In all of those cases auto-vectorization silently gives up, and — because it is silent — you have no signal that you left a 4× on the table. This is the gap the Vector API was built to close: a way to state your vector intent so the JIT maps it to hardware predictably, instead of hoping it infers it. Time to write one.
Feature
SuperWord (auto-vectorization)
Vector API
Automatic
Yes — free, no code
No — you write the loop
Guaranteed SIMD
No — best effort, can silently bail
Yes — you state the intent
Handles data-dependent branches
Usually no
Yes, via masks
Programmer control
None
Full (species, masks, reductions)
Readability
Excellent — it is just a for loop
Moderate — explicit API calls
The two are complementary, not rivals: lean on SuperWord by default, and drop to the Vector API exactly where SuperWord gives up. Everything below is about writing that explicit loop.
Hello, Vector API
The smallest useful program introduces the two types you cannot avoid: FloatVector (a vector of floats) and VectorSpecies (the description of a vector's shape). Here it is, run for real:
import jdk.incubator.vector.*;
public class VectorIntro {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
public static void main(String[] args) {
System.out.println("Preferred species: " + SPECIES);
System.out.println("Lane count (length): " + SPECIES.length());
float[] a = {1, 2, 3, 4, 5, 6, 7, 8};
float[] b = {10, 20, 30, 40, 50, 60, 70, 80};
FloatVector va = FloatVector.fromArray(SPECIES, a, 0);
FloatVector vb = FloatVector.fromArray(SPECIES, b, 0);
// One expression, all lanes: c = -(a*a + b*b)
FloatVector vc = va.mul(va).add(vb.mul(vb)).neg();
System.out.println("result = " + vc);
}
}
Compiled with javac --add-modules jdk.incubator.vector and run the same way, on an AVX2 machine it prints:
Preferred species: Species[float, 8, S_256_BIT]
Lane count (length): 8
result = [-101.0, -404.0, -909.0, -1616.0, -2525.0, -3636.0, -4949.0, -6464.0]
Three things to notice, because the rest of the API builds on them. SPECIES_PREFERRED asked the runtime for the widest shape this CPU supports and got back an eight-lane, 256-bit species — on an AVX-512 box the very same code would report sixteen lanes. fromArray loaded eight consecutive floats into a vector in one go. And the chained mul/add/neg computed all eight results with lane-wise operations, exactly the arithmetic our scalar loop did one element at a time. What this toy leaves out is how to handle an array whose length is not a tidy multiple of eight — which is every real array. Solving that cleanly requires understanding species properly.
Species, shapes, and lanes — the vocabulary
A VectorSpecies bundles two facts: the element type (here, float) and the shape (the register width in bits). From those it derives the lane count, which is simply shape divided by element size. Pinning down the four terms makes the rest of the API read easily:
Term
Meaning
Example (AVX2)
Element type
the primitive in each lane
float (32-bit)
Shape
register width in bits
256-bit (S_256_BIT)
Lane count / length
shape ÷ element size
256 ÷ 32 = 8
Species
element type + shape together
FloatVector.SPECIES_256
The same source, then, produces different lane counts on different hardware — which is precisely the point of asking for the preferred species instead of hard-coding a width:
CPU / instruction set
Vector width
Preferred float lanes
x86 SSE / AVX (128-bit)
128-bit
4
x86 AVX2 (256-bit)
256-bit
8 — the machine these benchmarks ran on
x86 AVX-512 (512-bit)
512-bit
16
Arm NEON (128-bit)
128-bit
4
Arm SVE (scalable)
runtime-dependent
resolved at run time
Write the loop against SPECIES_PREFERRED and species.length() and the same bytecode runs 4-wide on NEON, 8-wide on AVX2, and 16-wide on AVX-512 with no change — the portability the next section depends on.
You will almost always use SPECIES_PREFERRED, which resolves to the best shape for the running CPU, rather than hard-coding SPECIES_256. The portable habit is to never assume a lane count: read it from species.length() and let the same source run eight-wide on one machine and sixteen-wide on another. With that in hand, the real loop — the one that handles any array length — is straightforward.
Your first real vector loop: the loopBound + tail pattern
The idiom every Vector API loop uses is: stride through the array in lane-count-sized chunks for as long as a full vector fits, then finish the leftover elements with a plain scalar loop. The method species.loopBound(length) returns the largest multiple of the lane count that is ≤ length, giving you a safe upper bound for the vector part:
import jdk.incubator.vector.*;
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
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]);
}
}
Walk it once and the pattern sticks. The main loop advances i by SPECIES.length() each time; on each pass it loads a lane-width slice of a and b at offset i, does the lane-wise math, and stores the whole result vector back into c with intoArray. When fewer than a full vector's worth of elements remain, the main loop stops and the scalar tail mops up the rest — at most seven elements on an eight-lane machine. This is the exact code that produced the “Vector API” timings in the benchmark above, which is the honest place to pause and ask what those timings really told us.
The benchmark, honestly read
Collecting the two runs into one table makes the lesson unambiguous:
Loop (8192 floats)
C2 auto-vec ON
C2 auto-vec OFF
Scalar for
~869 ns
~3843 ns
Vector API
~967 ns
~877 ns
Vector API advantage
0.90× (slightly slower)
4.38× faster
The Vector API is not magic pixie dust that makes every loop faster — on a loop C2 already vectorizes, the explicit version can even lose by a hair. Its value is reliability: it delivers roughly the same ~880 ns whether or not the compiler cooperates, whereas the scalar loop swings by 4.4× depending on a decision you do not control and cannot see. For our tidy square-and-add loop that reliability is not worth the extra code. But recall the earlier list of loops C2 refuses to touch — the branchy, filtering, reducing ones. That is where explicit vectorization stops being insurance and starts being a genuine speedup, and it needs one more tool: masks.
What machine code did the JVM actually generate?
It is worth proving the abstraction is real rather than taking it on faith. Ask HotSpot to print the assembly it JIT-compiled for vectorComputation — -XX:+PrintAssembly with an hsdis disassembler plugin — and the inner loop that computes c = -(a² + b²) comes out as a short run of AVX2 instructions. This is the real disassembly, captured on the same AVX2 machine that produced the benchmarks above:
Each ymm register is 256 bits wide — eight 32-bit floats side by side — so a single vmulps multiplies eight pairs of floats at once. That one instruction is the hardware implementation of the FloatVector.mul() call; vaddps is .add(), and the two vmovdqus are fromArray and intoArray. One detail is more honest than the tidy version of the story: .neg() does not compile to a subtract. C2 negates a float by flipping its sign bit with vxorps against a mask of 0x80000000 values — cheaper than subtracting from zero, and a reminder that the API maps to whatever the compiler judges fastest, not to one fixed instruction. (C2 also unrolls the loop, emitting several of these ymm blocks per iteration; the excerpt shows one.) This is the bridge the whole post is about, made literal: one line of Java, seven instructions, eight lanes at a time.
Masks: branchless control flow across lanes
A branch inside a loop is poison for SIMD: the eight lanes of a vector must all execute the same instruction, so “do this for some elements but not others” cannot be expressed as an if. The Vector API's answer is a mask — a per-lane boolean, produced by a comparison, that says which lanes an operation should affect. Consider summing only the elements of an array that exceed a threshold. The scalar version has exactly the data-dependent branch that defeats auto-vectorization:
static final float THRESH = 0.5f;
// Scalar: the branch is data-dependent -> C2 will not vectorize this
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;
}
The vector version replaces the branch with a mask. v.compare(GT, THRESH) yields a VectorMask that is true in the lanes above the threshold; passing that mask to add accumulates only those lanes and leaves the rest untouched. No branch ever executes:
static final VectorSpecies<Float> SP = FloatVector.SPECIES_PREFERRED;
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 8 lanes
for (; i < a.length; i++) if (a[i] > THRESH) sum += a[i]; // masked-off tail
return sum;
}
Run head-to-head with the same warmup, on 8192 floats:
Nearly 4.8× faster — because this time C2 could not auto-vectorize the scalar loop, so the explicit mask is the only thing bringing SIMD to bear. Notice too the new step reduceLanes(ADD): after the loop, the accumulator vector holds eight partial sums (one per lane), and a reduction collapses them into the single scalar total. But look closely at the two sums. They are not identical: 3057.7180 versus 3057.7139. That tiny discrepancy is not a bug — it is a genuinely important property of vectorized floating-point, and glossing over it is how people ship subtly wrong numbers.
The floating-point reduction gotcha
Floating-point addition is not associative: (x + y) + z can differ from x + (y + z) in the last bits, because each intermediate result is rounded. The scalar loop adds the qualifying elements strictly left to right. The vector loop instead keeps eight running sums in the eight lanes and only combines them at the end, so the additions happen in a different order — and a different order means different rounding. Hence 3057.7180 versus 3057.7139: same mathematical answer, different accumulated rounding error. Seeing the two orders side by side makes the reason obvious:
Both compute the same sum, but the additions happen in a different order — and because floating-point rounding depends on order, the last bits differ. Original diagram.
For most workloads — graphics, machine learning, aggregate statistics — this difference is far below anything that matters. But if you require bit-for-bit reproducibility, or you are summing values of wildly different magnitudes where order genuinely changes the result, you must decide the ordering deliberately (for example with a compensated-summation algorithm) rather than letting lane count silently pick it for you. Integer operations have no such hazard: integer add is associative, so an integer reduction is exact regardless of lane order. Keeping this in view is what separates “it's faster” from “it's faster and still correct,” which brings us to the rest of the sharp edges.
Where you would actually reach for this: image brightness
Synthetic square-and-add loops make the mechanics clear, but the natural question is where this earns its keep. Image processing is the canonical fit, because every pixel can be transformed independently — exactly the shape SIMD wants. Take a brightness adjustment: scale each pixel by a gain, add a bias, and clamp the result back into the valid [0, 255] range.
// Scalar: the clamp is two data-dependent branches -> C2 will not vectorize this
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;
}
}
The clamp is the crux: those two ifs are exactly the data-dependent branches that stop auto-vectorization cold. In the Vector API version the branches vanish, because clamping is just a lane-wise max followed by min, and the multiply-add folds into a single fma:
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;
}
}
Benchmarked the same way as everything else here, on 8192 pixels:
Nearly 5×, and for the same reason as the masked-sum earlier: the branch defeats C2, so the explicit vector version is the only one actually running SIMD. The same shape covers the other workloads people reach for — pixel normalization, audio gain and mixing, cosine similarity over embedding vectors, small matrix kernels: contiguous data, independent elements, and often a clamp or threshold a plain loop cannot cross.
Performance pitfalls, and how the abstractions map to hardware
The Vector API is a thin, honest layer over real instructions, and every abstraction has a hardware cost you should be able to picture. Masks, comparisons, and simple lane-wise arithmetic are cheap — they are single instructions on any SIMD-capable core. Two operations are not cheap and deserve respect. Shuffles (VectorShuffle), which permute lanes, map to hardware permute instructions that are often several times slower than arithmetic; if you find yourself shuffling to rearrange data, it is almost always faster to store the data in the right layout to begin with — Structure-of-Arrays rather than Array-of-Structures — so loads are contiguous. Gather/scatter (indexed a[index[i]] access) is slower still and only vectorizes well on hardware that has dedicated instructions for it.
Beyond individual operations, four systemic pitfalls catch people:
You are memory-bound, not compute-bound. SIMD accelerates arithmetic per byte loaded. If your loop is limited by how fast data streams from memory — which large, once-touched arrays usually are — vectorizing the math buys little. Our benchmarks used an 8192-float (32 KB) array precisely so it stays in cache and the compute actually dominates.
Letting vectors escape. Vector objects are meant to be short-lived and optimized away into registers. Store them in a collection or field and you defeat the whole model; the JIT is forced to materialize real heap objects.
An operation your CPU lacks. The API degrades gracefully — if a shape or operation is not supported in hardware, it still runs correctly via a scalar fallback — but “correct” is not “fast.” A loop that looks vectorized can quietly run scalar on the wrong machine.
Measuring with a stopwatch. The numbers in this post used long warmups to force C2 compilation, but they are still directional. For decisions you will defend, use JMH, which handles warmup, dead-code elimination, and statistics properly.
Internalizing these turns the Vector API from a curiosity into a tool you can point at the few loops that deserve it. The last question is where the tool itself is headed.
Common mistakes
Most Vector API disappointments trace back to a short list of recurring errors. Keep it nearby:
Vectorizing loops that are not hot. If a profiler has not flagged the loop, rewriting it in SIMD adds complexity and risk for speed no user will ever notice. Optimize the few loops that dominate; leave the rest readable.
Ignoring memory bandwidth. SIMD accelerates arithmetic, not the memory bus. A loop that streams a large array once and does little math per element is bandwidth-bound, and vectorizing the math buys almost nothing.
Benchmarking without JMH. Hand-rolled timing loops fall prey to dead-code elimination, missing warmup, and on-stack replacement. (While writing this post, a naive cosine-similarity timing loop reported single-digit nanoseconds — because the JIT deleted the entire computation as unused.) Use JMH for any number you intend to defend.
Expecting bit-for-bit identical floating-point reductions. Lane-wise reductions reorder additions, so vector and scalar sums differ in the last bits. Fine for most work; wrong if you require exact reproducibility.
Assuming every operation maps to one instruction. Shuffles, gather/scatter, and operations your CPU lacks can expand into many instructions or fall back to scalar. “Fast on paper” is not “fast on this machine.”
A copy-paste JMH benchmark
Because that third mistake is the most common, here is a minimal JMH harness for the square-and-add kernel — enough to drop into a project and measure honestly. It compiles cleanly against jmh-core 1.37 on JDK 21:
import jdk.incubator.vector.*;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class VectorBench {
static final VectorSpecies<Float> SP = FloatVector.SPECIES_PREFERRED;
float[] a, b, c;
@Setup
public void setup() {
int n = 8192;
a = new float[n]; b = new float[n]; c = new float[n];
for (int i = 0; i < n; i++) { a[i] = (float) Math.random(); b[i] = (float) Math.random(); }
}
@Benchmark
public void scalar() {
for (int i = 0; i < a.length; i++) c[i] = -(a[i]*a[i] + b[i]*b[i]);
}
@Benchmark
public void vector() {
int i = 0, bound = SP.loopBound(a.length);
for (; i < bound; i += SP.length()) {
var va = FloatVector.fromArray(SP, a, i);
var vb = FloatVector.fromArray(SP, b, i);
va.mul(va).add(vb.mul(vb)).neg().intoArray(c, i);
}
for (; i < a.length; i++) c[i] = -(a[i]*a[i] + b[i]*b[i]);
}
}
Writing results into the c[] field (rather than a local the JIT can prove is dead) is what keeps them observable and stops the dead-code elimination that wrecked the naive timing loop. Run it with the incubator module on the command line: java --add-modules jdk.incubator.vector -jar target/benchmarks.jar.
Where JEP 537 is going: incubation and Valhalla
The Vector API has been incubating longer than almost any feature in Java's history — twelve rounds, from JDK 16 through the JDK 27 proposal in JEP 537, with no substantial changes in the most recent rounds. That is not drift; it is a deliberate wait. The API models each vector as an object, and its performance depends on the JIT reliably flattening those objects into registers. Project Valhalla's value classes — which give the JVM exactly that guarantee, with no identity and no heap header — are the missing foundation. The plan is explicit: incubate until Valhalla's value objects are available as a preview feature, then re-base the Vector API on them and promote it from incubator to preview. (If value classes are new to you, they are the subject of a companion guide on this site, linked in the references — the two features are two halves of the same performance story.)
There is real engineering happening under the surface even without an API change. The JDK 27 proposal, for instance, updates the bundled SLEEF library — used to implement vector math functions like sin and exp on Arm and RISC-V — from version 3.6.1 to 3.9.0, quietly improving transcendental performance on those architectures. And on the auto-vectorization side, C2's SuperWord continues to improve release over release, so the “free lunch” keeps getting bigger too. Both paths — implicit and explicit — are advancing together, which is the right frame for deciding which one to reach for.
A decision guide: which path should you use?
Your loop…
Reach for…
Why
is a simple counted loop over contiguous arrays with straight-line math
Nothing — trust C2
SuperWord already vectorizes it; explicit code adds ceremony for no gain
has data-dependent branches, filtering, or thresholding
Vector API + masks
C2 will not vectorize the branch; masks make it branchless (our 4.8× case)
needs guaranteed SIMD across machines and JDK versions
Vector API
reliability: same speed regardless of C2's discretion
lane reductions reorder additions and change rounding
The same logic reads even faster as a flowchart — a quick decision path for any hot loop:
A practical decision path: most hot loops exit early to “leave it scalar,” and the Vector API is reserved for branchy or filtering loops that C2 will not cross. Original diagram.
The through-line is that vectorization is not a switch you flip on everything; it is a targeted tool for arithmetic-heavy, cache-resident, branch-or-not loops that a profiler has flagged as hot.
Quick reference
// Setup: compile and run with the incubator module
// javac --add-modules jdk.incubator.vector Foo.java
// java --add-modules jdk.incubator.vector Foo
import jdk.incubator.vector.*;
// The species: element type + shape. Prefer PREFERRED for portability.
static final VectorSpecies<Float> SP = FloatVector.SPECIES_PREFERRED;
SP.length(); // lanes on THIS cpu (8 on AVX2, 16 on AVX-512)
SP.loopBound(n); // largest multiple of length() that is <= n
// Load / compute / store
var v = FloatVector.fromArray(SP, arr, i); // load a lane-width slice
var r = v.mul(v).add(other).neg(); // lane-wise arithmetic
r.intoArray(out, i); // store the whole vector
// Masks: branchless conditionals
VectorMask<Float> m = v.compare(VectorOperators.GT, threshold);
acc = acc.add(v, m); // apply only where m is true
v.blend(other, m); // pick lanes from v or other
// Reductions: many lanes -> one scalar (watch FP ordering!)
float sum = acc.reduceLanes(VectorOperators.ADD);
// The universal loop shape
int i = 0, bound = SP.loopBound(arr.length);
for (; i < bound; i += SP.length()) { /* vector body */ }
for (; i < arr.length; i++) { /* scalar tail */ }
Closing thought
The distance between the loop you write and the CPU you own has always been real, and for most of Java's life the only bridge was hoping the JIT would notice. That free bridge, C2's auto-vectorization, is better than ever and should remain your default — when it fires, you win with zero effort. The Vector API is the bridge you build yourself for the loops the compiler refuses to cross: the ones with masks, filters, and reductions, where the difference is not 10% but 4×. Start by learning to tell whether your hot loop is already vectorized (-XX:-UseSuperWord is a one-line experiment). Then, for the few that are not and should be, express the intent explicitly — carefully, with an eye on memory bandwidth and floating-point ordering — and let one instruction finally do the work of eight.