Java Module 26 – Advanced language and platform features
SIMD with the Vector API (incubator)
Write SIMD loops in Java with the incubating Vector API: species, lanes, tails and reductions, run on JDK 25 and measured against plain loops with JMH.
What you will learn
- Run code that uses an incubator module with --add-modules jdk.incubator.vector
- Write a vectorised total with a species, a loop bound and a tail, and check it against a scalar loop
- Compare the Vector API, the JIT's own auto-vectorisation and a plain loop with JMH
- Use masks and saturating operations for pixel data without wrap-around bugs
- Explain why an incubating API is not ready for production code
Before you start
What you’ll learn · Before you start
What you will learn
- Run code that uses an incubator module with --add-modules jdk.incubator.vector
- Write a vectorised total with a species, a loop bound and a tail, and check it against a scalar loop
- Compare the Vector API, the JIT's own auto-vectorisation and a plain loop with JMH
- Use masks and saturating operations for pixel data without wrap-around bugs
- Explain why an incubating API is not ready for production code
Before you start
On this page
SIMD, single instruction, multiple data, means one processor instruction working on several numbers at once: one
addition on two vectors of four ints performs four additions. The Vector API, in the incubator module
jdk.incubator.vector, lets Java code say exactly which vector operations it wants, and the HotSpot C2 compiler turns
them into the CPU’s vector instructions: NEON or SVE on arm64, SSE or AVX on x64. It is still an incubating API: JDK 25
carries its tenth round (JEP 508), JDK 26 its eleventh (JEP 529) and JDK 27 its twelfth (JEP 537), and JEP 537 says it
will keep incubating until the features of Project Valhalla it needs are available as previews. It suits hot numeric
loops that the JIT does not vectorise by itself; it needs an extra option to compile and run, and its package name
will change when it is promoted. This lesson writes a vector loop, runs it on JDK 25, and checks it against a plain
loop.
Version note
Incubator. jdk.incubator.vector needs --add-modules jdk.incubator.vector when compiling and when running,
and its package will be renamed when the API is promoted. Everything here was measured with Temurin 25.0.4.1 on an
arm64 Mac, whose NEON unit works on 128-bit vectors.
Species, lanes and the shape of a vector loop
A few words cover the whole API:
- A lane is one element of a vector; a 128-bit vector of
ints has 4 lanes. - A shape is a vector’s size in bits: 64, 128, 256, 512, or the largest the CPU has.
- A species combines an element type and a shape.
IntVector.SPECIES_PREFERREDasks for the CPU’s best size. - A lane-wise operation, such as
add, works on each lane separately; a reduction, such asreduceLanes(ADD), combines the lanes of one vector into a single value. loopBound(length)is the largest multiple of the lane count that fits, and the tail is what remains after it.- A mask switches lanes on or off, which lets one vector operation cover a partial vector at the end.
Ten amounts, four lanes, one reduction
Text description of the diagram
The diagram has three parts, from top to bottom, joined by arrows.
The first part is an array of ten int amounts processed with four int lanes, a 128-bit vector. Vector 1 holds elements 0 to 3 and vector 2 holds elements 4 to 7. Elements 8 and 9 are the tail: loopBound(10) is 8, the largest multiple of four that fits, so the last two elements are handled by an ordinary scalar loop or by a masked load.
An arrow labelled "add, lane by lane" leads to the second part, a vector of four running totals, one per lane: lane 0 holds a0 plus a4, lane 1 holds a1 plus a5, lane 2 holds a2 plus a6 and lane 3 holds a3 plus a7.
An arrow leads to the third part, the total: reduceLanes(ADD) adds the four lanes together once, after the loop, and the tail is added to the result.
A first vector loop, run
Totals.java adds up 1,003 amounts in paise twice, with a plain loop and with four lanes:
// Adds up 1,003 amounts in paise twice: with a plain loop, and four lanes at a time with the Vector API.
import jdk.incubator.vector.IntVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
// The widest int vector this CPU supports well: on an arm64 Mac, 128 bits, so 4 int lanes.
static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;
static int scalarTotal(int[] paise) {
int total = 0;
for (int p : paise) total += p;
return total;
}
static int vectorTotal(int[] paise) {
IntVector lanes = IntVector.zero(SPECIES); // one running total per lane
int i = 0;
int upper = SPECIES.loopBound(paise.length); // the largest multiple of the lane count
for (; i < upper; i += SPECIES.length()) {
lanes = lanes.add(IntVector.fromArray(SPECIES, paise, i)); // load 4 amounts, add them lane by lane
}
int total = lanes.reduceLanes(VectorOperators.ADD); // add the lanes together, once
for (; i < paise.length; i++) total += paise[i]; // the tail: what did not fill a whole vector
return total;
}
void main() {
IO.println("species: " + SPECIES + ", " + SPECIES.length() + " int lanes");
int[] paise = new int[1_003];
for (int i = 0; i < paise.length; i++) paise[i] = 100 + (i * 37) % 900; // amounts from 100 to 999 paise
IO.println("loop bound for " + paise.length + " amounts: " + SPECIES.loopBound(paise.length) + ", tail: " + (paise.length - SPECIES.loopBound(paise.length)));
IO.println("scalar total: " + scalarTotal(paise));
IO.println("vector total: " + vectorTotal(paise));
} from session import run
# javac and java both need the incubator module added; both say so with a warning.
run("""
javac --add-modules jdk.incubator.vector -d out Totals.java
java --add-modules jdk.incubator.vector -cp out Totals
""") Output
# JDK in use: openjdk version "25.0.4.1" LTS $ javac --add-modules jdk.incubator.vector -d out Totals.java warning: [incubating] using incubating module(s): jdk.incubator.vector 1 warning $ java --add-modules jdk.incubator.vector -cp out Totals WARNING: Using incubator modules: jdk.incubator.vector species: Species[int, 4, S_128_BIT], 4 int lanes loop bound for 1003 amounts: 1000, tail: 3 scalar total: 548911 vector total: 548911
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 totals_session.py
What the run shows:
- The species fits the machine. On this arm64 Mac the preferred
intspecies is 128 bits, 4 lanes. A CPU with 256-bit vectors would report 8 lanes for the same code, which is why the loop usesSPECIES.length()and never a fixed 4. - The tail is real.
loopBound(1003)is 1000, so 250 vector additions cover the first 1,000 amounts and a plain loop adds the last 3. - Both totals agree, 548,911 paise, because integer addition gives the same result in any order.
- Two warnings.
javacwarns that it is using an incubating module, and the JVM printsWARNING: Using incubator modulesbefore the program starts. JEP 11 requires both.
The Vector API, auto-vectorisation and a plain loop
HotSpot’s C2 compiler can already turn some ordinary loops into vector instructions by itself, which is called auto-vectorisation. The JEPs of the Vector API describe that as limited and fragile, which is the reason the API exists:
| Approach | Who chooses SIMD, where it fits, what can break it |
|---|---|
| A plain loop | Nobody: one element at a time, for any code, with nothing to break and no option needed |
| The same loop vectorised by C2 | The JIT, when the loop’s shape allows it: simple loops over arrays, element by element; a small change to the loop can undo it |
| The Vector API | You, in the code: reductions, masks and rearranging lanes; it needs --add-modules jdk.incubator.vector and changes with each incubator round and at promotion |
Three things decide whether the API is worth it for a loop, and the lesson measures each of them:
- JMH numbers show when the API beats a plain loop and when C2 had already vectorised it.
- Floating-point order changes a float total when its additions are split across lanes.
- Saturating arithmetic keeps pixel values from wrapping around at 255.
- C1 and the interpreter run vector code without SIMD at all, far slower than a plain loop.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Measured with JMH: where the Vector API wins, and where the JIT already won Pro: opens with a Pro, Premium or Ultimate pass
Speed claims about SIMD are only as good as the measurement behind them. This part benchmarks two loops in both styles with the JIT’s auto-vectorisation on and off, then shows that a faster float sum is also a different one.
In this part:
- Two loops, four benchmarks, two JIT settings
- Faster, and a different float result
5 runnable examples (Java, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Masks and saturating arithmetic: an image row brightened without wrap-around Pro: opens with a Pro, Premium or Ultimate pass
Pixel work is where SIMD pays off most often, and where a lane-wise add goes wrong most quietly. This part brightens a row of grey pixels three ways, finds the wrap-around bug, fixes it, and sets the benchmark report as an exercise with a rubric.
In this part:
- A wrap-around bug, reproduced
- Exercise: vectorise, measure and report
2 runnable examples (Java, Python) · about 2 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
When not to use it: C1, incubator rules and what changes at promotion Pro: opens with a Pro, Premium or Ultimate pass
A vector loop that is fast under C2 can be very slow anywhere else, and an incubating API brings its own costs. This part measures the first and lists the second, with a design that keeps both in check.
In this part:
- Without C2, measured
- The rules that come with an incubator module
- A design that keeps the risk small
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
-
Fresher to mid · Warm-up
What are a species, a lane and a tail in Java's Vector API?
A lane is one element of a vector: a 128-bit vector of ints has 4 lanes, and lane-wise operations such as add work on all of them in one step. A species is the combination of an element type and a vector size, such as IntVector.SPECIES_128 or IntVector.SPECIES_PREFERRED, the best size for the CPU the program runs on; it says how many lanes a vector has and how to load one from an array. A loop over an array steps by species.length() up to species.loopBound(length), the largest multiple of the lane count that fits. The tail is what is left after that bound, fewer elements than one vector holds. It is handled by an ordinary scalar loop, or by a mask from indexInRange that switches off the lanes past the end of the array.
-
Mid to senior
A colleague rewrote an element-wise loop that scales prices, using the Vector API, and the benchmark shows no speed-up at all. The code is correct. What are the likely explanations, and how would you check each one?
-
Mid to senior
After a daily-sales report was vectorised with the Vector API, its float totals differ from the old scalar report in the last digits, and for one large region by several thousand rupees. Is the vector code wrong, and what do you change?
-
Senior to staff
Your team measured a threefold speed-up in an image-filter service by using jdk.incubator.vector on JDK 25. The service deploys weekly and must keep working through JDK upgrades. Would you ship it, and under what conditions?
Key takeaways
- The Vector API (
jdk.incubator.vector) compiles explicit vector operations to SIMD instructions in C2; it is in its tenth incubating round in JDK 25 and twelfth in JDK 27. - A vector loop has a species, a main loop up to
loopBoundand a tail, and reduces lanes once, after the loop. - Compile and run with
--add-modules jdk.incubator.vector; bothjavacand the JVM warn. - C2 already vectorises simple element-wise loops, so measure against a plain loop before writing vector code.
References
- JEP 508: Vector API (Tenth Incubator) (OpenJDK)
- JEP 529: Vector API (Eleventh Incubator) (OpenJDK)
- JEP 537: Vector API (Twelfth Incubator) (OpenJDK)
- JEP 11: Incubator Modules (OpenJDK)
- Package jdk.incubator.vector (JDK 25 API) (Oracle)
- VectorOperators (JDK 25 API) (Oracle)
- The java Command (JDK 25 tool reference) (Oracle)
- The javac Command (JDK 25 tool reference) (Oracle)
- Java Microbenchmark Harness (JMH): README and samples (OpenJDK)
- The Java Language Specification, Java SE 25: 15.7.3 Evaluation Respects Parentheses and Precedence (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress