Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 22 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 22 minutes free
Free: Species, lanes and the shape of a vector loop · A first vector loop, run · The Vector API, auto-vectorisation and a plain loop. With a pass: 3 in-depth parts and 3 interview answers, about 15 minutes.

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_PREFERRED asks for the CPU’s best size.
  • A lane-wise operation, such as add, works on each lane separately; a reduction, such as reduceLanes(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.
A vector loop over 10 ints with 4 lanes: two full vectors, a tail of 2, lane totals reduced once at the end.int[10] amounts, 4 int lanes4 running totals, one per lanereduceLanes(ADD) once,plus the tail: the totalvector 1: elements 0 to 3vector 2: elements 4 to 7tail: elements 8 and 9loopBound(10) = 8lane 0a0 + a4lane 1a1 + a5lane 2a2 + a6lane 3a3 + a7add, lane by lane

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:

Totals.java Java · Totals.java
// 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));
}
Compiled and run on JDK 25 with the incubator module Python · totals_session.py
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 int species is 128 bits, 4 lanes. A CPU with 256-bit vectors would report 8 lanes for the same code, which is why the loop uses SPECIES.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. javac warns that it is using an incubating module, and the JVM prints WARNING: Using incubator modules before 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.
Descriptive Statistics Calculator Paste the scores of several JMH runs to see their mean and spread before you compare two versions of a 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.

  1. Question 1 of 5 A species has 8 int lanes. What does loopBound(1003) return?

    Type a number.

    Show the answer to question 1

    Answer: 1000

    loopBound returns the largest multiple of the lane count that is not greater than the length: 1003 divided by 8 is 125 with 3 left over, and 125 × 8 = 1000. The main loop covers elements 0 to 999 and the tail handles the last 3. With the 4 lanes of the lesson's machine the answer is also 1000, with a tail of 3.

  2. Question 2 of 5 Why does Totals.java need --add-modules jdk.incubator.vector both when compiling and when running?

    Choose one answer.

    Show the answer to question 2

    Answer: Incubator modules are not resolved by default, so javac and java must each be asked to add the module

    JEP 11 keeps incubator modules out of the default set of root modules, so code cannot use them by accident. javac needs the module to see the package, and the JVM needs it to load the classes; both print a warning when it is used. The CPU instructions are chosen by the JIT compiler, not by an option.

  3. Question 3 of 5 On the lesson's arm64 machine, IntVector.SPECIES_PREFERRED printed Species[int, 4, S_128_BIT]. What does that say?

    Choose one answer.

    Show the answer to question 3

    Answer: The preferred vector is 128 bits wide, so it holds 4 ints of 32 bits each

    A species combines an element type and a shape. S_128_BIT is the vector size in bits; 128 / 32 = 4 lanes for int. A CPU with 256-bit or 512-bit vectors would report 8 or 16 int lanes for the same code.

  4. Question 4 of 5 Totals.java adds each loaded vector into a vector of lane totals and calls reduceLanes only after the loop. Why not reduce every vector inside the loop?

    Choose one answer.

    Show the answer to question 4

    Answer: A reduction adds across lanes, which is slower than lane-wise adds; doing it once keeps the loop lane-wise

    Lane-wise operations work on all lanes at once, while a cross-lane reduction combines the lanes of one vector. Keeping one running total per lane and reducing once moves that cross-lane work out of the loop. For ints both versions give the same total, because integer addition is associative.

  5. Question 5 of 5 Which of these are reasons why the Vector API in JDK 25 should not go into production code without care?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • It is an incubating API whose packages are renamed when it is promoted
    • It needs --add-modules when compiling and running, and prints a warning
    • Its JEP says it will incubate until Project Valhalla features are available as previews

    JEP 11 says incubating APIs can change or be removed and are renamed on promotion, the module must be added explicitly and a warning is printed, and JEP 508 and JEP 537 tie the end of incubation to Valhalla. Integer results are exact; the lesson's two totals agree.

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.

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.

  1. 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.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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 loopBound and a tail, and reduces lanes once, after the loop.
  • Compile and run with --add-modules jdk.incubator.vector; both javac and the JVM warn.
  • C2 already vectorises simple element-wise loops, so measure against a plain loop before writing vector code.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.