向量化
下面这段代码可以如何优化?
void foo(byte[] dst, byte[] src) {
for (int i = 0; i < dst.length - 4; i += 4) {
dst[i] = src[i];
dst[i+1] = src[i+1];
dst[i+2] = src[i+2];
dst[i+3] = src[i+3];
}
... // post-loop
}
- X86_64 平台不支持内存间的直接移动,上面代码中的dst[i] = src[i]通常会被编译为两条内存访问指令
- 第一条指令把src[i]的值读取至寄存器中
- 第二条指令则把寄存器中的值写入至dst[i]中
- 上述代码一个循环迭代将会执行四条内存读取指令,以及四条内存写入指令
- 数组元素在内存中是连续的,当从src[i]的内存地址处读取 32 位的内容时,我们将一并读取src[i]至src[i+3]的值
- 当向dst[i]的内存地址处写入 32 位的内容时,我们将一并写入dst[i]至dst[i+3]的值
- 通过综合这两个批量操作,我们可以使用一条内存读取指令以及一条内存写入指令,完成上面代码中循环体内的全部工作
//优化后代码:x[i:i+3]指代x[i]至x[i+3]合并后的值
void foo(byte[] dst, byte[] src) {
for (int i = 0; i < dst.length - 4; i += 4) {
dst[i:i+3] = src[i:i+3];
}
... // post-loop
}
SIMD 指令
- 在前面的示例中,我们使用的是 byte 数组,四个数组元素并起来也才 4 个字节
- 如果换成 int 数组,或者 long 数组,那么四个数组元素并起来将会是 16 字节或 32 字节
- X86_64 体系架构上通用寄存器的大小为 64 位(即 8 个字节),无法暂存这些超长的数据
- 编译器需借助长度足够的 XMM 寄存器,来完成 int 数组与 long 数组的向量化读取和写入操作
- XMM寄存器:
- 由 SSE(Streaming SIMD Extensions)指令集所引入的
- 一开始仅为 128 位。自从 X86 平台上的 CPU 开始支持 AVX(Advanced Vector Extensions)指令集后(2011 年),XMM 寄存器便升级为 256 位,并更名为 YMM 寄存器
- 原本使用 XMM 寄存器的指令,现将使用 YMM 寄存器的低 128 位
- 单指令流多数据流(Single Instruction Multiple Data,SIMD),即通过单条指令操控多组数据的计算操作。这些指令我们称之为 SIMD 指令
-
SIMD 指令将 XMM 寄存器(或 YMM 寄存器、ZMM 寄存器)中的值看成多个整数或者浮点数组成的向量,并且批量进行计算
XMM寄存器示意图 - 128 位 XMM 寄存器里的值可以看成 16 个 byte 值组成的向量,或者 8 个 short 值组成的向量,4 个 int 值组成的向量,两个 long 值组成的向量
- SIMD 指令PADDB、PADDW、PADDD以及PADDQ,将分别实现 byte 值、short 值、int 值或者 long 值的向量加法
- 看如下方法:
void foo(int[] a, int[] b, int[] c) {
for (int i = 0; i < c.length; i++) {
c[i] = a[i] + b[i];
}
}

优化后示意图
- 原本需要c.length次加法操作的代码,现在最少只需要c.length/4次(理论上)向量加法即可完成。因此,SIMD 指令也被看成 CPU 指令级别的并行
使用 SIMD 指令的 HotSpot Intrinsic
- SIMD 指令虽然非常高效,但是使用起来却很麻烦
- 主要是因为不同的 CPU 所支持的 SIMD 指令可能不同
- 越新的 SIMD 指令,它所支持的寄存器长度越大,功能也越强
- 为了能够尽量利用新的 SIMD 指令,我们需要提前知道程序会被运行在支持哪些指令集的 CPU 上,并在编译过程中选择所支持的 SIMD 指令中最新的那些
- 我们可以在编译结果中纳入同一段代码的不同版本,每个版本使用不同的 SIMD 指令。在运行过程中,程序将根据 CPU 所支持的指令集,来选择执行哪一个版本
- Java 字节码的平台无关性,Java 程序无法像 C++ 程序那样,直接使用由 Intel 提供的,将被替换为具体 SIMD 指令的 intrinsic 方法
- 替代方案是 Java 层面的 intrinsic 方法,这些 intrinsic 方法的语义要比单个 SIMD 指令复杂得多
- 在运行过程中,HotSpot 虚拟机将根据当前体系架构来决定是否将对该 intrinsic 方法的调用替换为另一高效的实现。如果不,则使用原本的 Java 实现
自动向量化
- 即时编译器的自动向量化将针对能够展开的计数循环,进行向量化优化
- 自动向量化的条件
- 循环变量的增量应为 1,即能够遍历整个数组
- 循环变量不能为 long 类型,否则 C2 无法将循环识别为计数循环
- 循环迭代之间最好不要有数据依赖,例如出现类似于a[i] = a[i-1]的语句。当循环展开之后,循环体内存在数据依赖,那么 C2 无法进行自动向量化
- 循环体内不要有分支跳转
- 不要手工进行循环展开。如果 C2 无法自动展开,那么它也将无法进行自动向量化
