如果程序里存在大量对浮点数组做加减乘除的循环,而每次循环只处理一个元素,那么 CPU 的大部分并行计算单元都在闲置。JDK 17 通过孵化模块 jdk.incubator.vector 引入 Vector API,可以显式把一个数组片段加载到向量寄存器中,用一条 SIMD 指令同时处理多个数据,从而缩短数值密集计算的执行时间。与依赖热点编译器自动向量化相比,Vector API 的可控性更强;与 JNI 调用本地库相比,它又不需要维护外部构建脚本和 native 代码。

一、Vector API 的核心抽象:Species 与 Vector
Vector API 并不是把某个固定宽度的寄存器暴露给开发者,而是通过 VectorSpecies 描述一种向量形状和元素类型。比如 FloatVector.SPECIES_PREFERRED 表示当前 CPU 最推荐的单精度浮点向量。程序运行时会根据硬件能力自动选择对应的向量长度:在支持 AVX2 的平台上,FloatVector 通常一次处理 8 个 float;在支持 AVX-512 的平台上,可能一次处理 16 个 float。硬编码通道数会让代码在不同硬件上的行为不一致,因此官方推荐优先使用 SPECIES_PREFERRED。
Vector 对象本身是不可变的,所有向量运算都会返回一个新的 Vector 实例。每次调用 fromArray 都会从数组中连续加载一段元素,生成一个向量;add、mul、fma 等方法则返回新的计算结果;intoArray 负责把向量写回数组。这种不可变设计避免了原地修改带来的副作用,也让 JIT 编译器更容易做逃逸分析和寄存器分配。
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
public class VectorConfig {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
public static void main(String[] args) {
System.out.println("向量长度: " + SPECIES.length());
System.out.println("元素类型: " + SPECIES.elementType());
System.out.println("位宽: " + SPECIES.vectorBitSize());
}
}
运行这段代码会打印当前平台推荐的向量长度。如果输出的 length 是 8,说明浮点向量寄存器为 256 位;如果是 16,则说明可利用 512 位宽。了解这个数值有助于判断某段计算是否值得向量化。一个只有 4 个 float 的数组显然不足以填满向量寄存器,需要靠尾部逻辑处理。
二、从标量循环迁移到向量循环
先看一个最常见的数组逐元素加法。标量版本如下,循环每次读取两个 float,计算一个加法,再写回一个 float。现代 CPU 执行这种循环时,如果热点编译器能自动向量化,简单情况也能获得一定加速;但一旦循环体里加入条件判断、方法调用或非连续访问,自动向量化就很容易失效。
public static void addScalar(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i];
}
}
使用 Vector API 的版本如下。主循环不再每次只前进 1,而是每次前进 SPECIES.length() 个元素。loopBound 会返回不超过数组长度的最大向量索引,保证主循环不会越界。fromArray 会从数组下标 i 开始连续加载一组 float,add 完成一组加法,intoArray 把结果一次写回。
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
public class VectorAdd {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
public static void addVector(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
FloatVector va = FloatVector.fromArray(SPECIES, a, i);
FloatVector vb = FloatVector.fromArray(SPECIES, b, i);
FloatVector vc = va.add(vb);
vc.intoArray(c, i);
}
for (; i < a.length; i++) {
c[i] = a[i] + b[i];
}
}
}
尾部元素不能填满一个完整向量,因此第二个标量循环处理长度除以向量通道数后的剩余部分。这种先向量主循环、后标量尾部的模式是最常用写法。也可以用 VectorMask 一次性处理尾部,代码会更紧凑,但通常只有在数组长度非常大、尾部占比很低时才值得使用。
为什么不让所有循环都走 mask 分支?原因在于 mask 的生成和带掩码的加载写回指令比普通向量指令稍重,主循环不携带 mask 可以获得更稳定的指令流水。对于数组加法这种热路径,清晰的主循环加尾循环性价比更高。
三、FMA 与向量累加器优化点积
数值计算中不止有加法,乘加融合指令 FMA 能显著提升吞吐并减少舍入误差。JDK 17 的 FloatVector 提供 fma 方法,对应硬件层面的 a * b + c 三元运算。如果计算表达式正好是这种形式,直接调用 va.fma(vb, vc) 比先 mul 再 add 更合适。很多 CPU 的 FMA 指令延迟与普通乘法或加法相近,但一条指令完成两步运算,能降低指令数量;同时只在最后执行一次舍入,数值精度也优于先乘后加。
以点积为例,如果写成一个标量 sum 在循环里累加,会破坏向量化收益,因为每次迭代都依赖上一步的标量结果。更合理的做法是维护一个向量累加器 acc,把每一轮的乘积向量累加进去,最后再横向归约。
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
public class DotProduct {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
public static float dot(float[] a, float[] b) {
FloatVector acc = FloatVector.zero(SPECIES);
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
FloatVector va = FloatVector.fromArray(SPECIES, a, i);
FloatVector vb = FloatVector.fromArray(SPECIES, b, i);
acc = acc.add(va.mul(vb));
}
float sum = acc.reduceLanes(VectorOperators.ADD);
for (; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
}
向量累加器 acc 是一条向量寄存器,它在循环中不断累加。循环结束后通过 reduceLanes 把各通道的值横向相加得到标量,这样避免了每轮迭代都做一次横向归约,减少依赖链长度。标量尾部仍然单独处理,保证结果正确。
如果计算过程中需要做乘加操作,比如 y = a * x + b,可以这样写:
FloatVector va = FloatVector.fromArray(SPECIES, a, i); FloatVector vx = FloatVector.fromArray(SPECIES, x, i); FloatVector vb = FloatVector.fromArray(SPECIES, b, i); FloatVector result = va.fma(vx, vb); result.intoArray(y, i);
这种写法把原本需要两次向量运算的乘法和加法合并成一条 FMA 向量指令,在高性能计算、矩阵运算、信号处理中非常实用。尤其当数组规模达到百万级时,减少一条向量指令意味着每次循环节省一次发射和调度成本,整体吞吐可以进一步提升。
四、编译运行参数与性能验证
JDK 17 中 Vector API 处于孵化状态,必须显式添加模块才能编译和运行。使用 javac 和 java 时都要带上 --add-modules jdk.incubator.vector,否则会报模块未找到或包不可见错误。不要把 jdk.incubator.vector 当成普通第三方包放到 classpath 里,它不是外部依赖,而是 JDK 自带的孵化模块。
javac --add-modules jdk.incubator.vector VectorAdd.java java --add-modules jdk.incubator.vector VectorAdd
在 IDE 中同样需要给编译器和运行配置添加该模块参数。如果使用 Maven 或 Gradle 构建,也要在编译插件和运行配置中传递对应的 JVM 参数,否则会出现找不到 jdk.incubator.vector 包的编译错误。
性能测试尽量用 JMH,避免使用 System.nanoTime 的少量采样。预热阶段会让 JIT 生成向量化代码,如果测试只跑几次,得到的结果反映的是解释执行和早期编译,没有参考意义。数组长度应该足够大,比如 100 万个 float,这样既能覆盖主循环,又能让内存访问模式稳定下来。
下面是一个基于 JMH 的简要对比结果,数组长度设置为 100 万个 float,每次迭代完成一次数组加法,测试环境支持 256 位 SIMD 向量。
| 实现方式 | 吞吐量(ops/s) | 相对提升 |
|---|---|---|
| 标量循环 | 约 3120 | 1.00x |
| Vector API | 约 9210 | 2.95x |
这个提升幅度只代表特定硬件上的一组结果,实际表现取决于 CPU 支持的 SIMD 宽度、内存带宽、数组是否在 L1 或 L2 缓存以及 JVM 版本。不要把它当作固定承诺。常见误区包括:数组太短时向量化收益会被循环启动成本吃掉;每次循环都重新创建 VectorSpecies;在冷路径上做向量计算。把数据规模、热点识别清楚,再决定是否引入 Vector API,通常会比盲目替换循环更有效。
Vector APIJDK 17SIMD指令修改时间:2026-09-25 14:56:44