1. 简介
1.1 SIMD
通常,程序的代码是串行执行的。这意味着单个命令或语句按顺序执行,一个接一个。以算术为重点的程序是对数字进行大量计算的程序。通常,这些程序处理大量数据,并且许多信息以相同的方式一个接一个地处理。例如。对于 1000 个粒子的模拟,模拟中将有一个步骤,用其当前速度 v 更新每个粒子的位置 s:s = s + v。这必须对每个粒子完成,即对粒子数组 s [0] 到 s[999]。
如果您可以将几个粒子组合成一个批次并一次性处理该批次,则可以加快速度。对于上面的示例和一组 4 个粒子,这意味着将计算分组如下:
s[0] = s[0] + v[0];
s[1] = s[1] + v[1];
s[2] = s[2] + v[2];
s[3] = s[3] + v[3];
更一般地说,这一次对多个数据元素执行一种操作。在汇编代码级别,有专门针对这些分组操作的指令。因此这个概念称为单指令多数据,简称SIMD。
在 x86 CPU 上,+ 的 SIMD 指令称为 addps(SSE 指令集)或 vaddps(AVX 指令集)。它需要两个组作为操作数,其中每个组有 4 个元素 (SSE) 或 8 个元素 (AVX)。它将一组的每个元素添加到另一组的相应元素。在上面的例子中,s[0…3] 是一个组,v[0…3] 是另一个组。生成的 x86 汇编代码是:
addps %xmm0,%xmm1 ;add vector in xmm0 to vector in xmm1, store result in xmm0
1.2. Vectorization
SIMD 是从指令设计者(即 CPU 制造商)的角度给出的概念名称。但这并不是唯一的观点。在数学中,具有固定数量元素(s[0…3] 和 v[0…3])的有序群被称为向量。因此 SIMD 指令也称为向量指令。这只是同一件事的另一个角度,这次是从用户的说明。
向量化是使用向量指令来加速程序执行。向量化可以由程序员完成,或者向量化的可能性可以由编译器自动实现。在后一种情况下,它称为自动向量化。
Auto vectorization is a kind of code optimization which is done by a compiler, either by an AOT compiler at compile time, or by a JIT compiler at execution time.
1.3. Vector Instructions in Java
编写 Java 程序后,Java 文件中的 Java 源代码被编译为字节码并保存到类文件中。然后,在程序执行之前或执行期间,通常会再次编译其字节码,这次是从字节码转换为本地机器码。后一种编译通常在程序执行时完成,因此它是 JIT 编译。
在 Java 中,目前向量化不是由程序员完成的,而是由编译器自动完成的。编译器接受标准的 Java 字节码并自动确定哪一部分可以转换为向量指令。 OpenJDK 或 Oracle 的 Java 等常见 Java 环境可以生成向量化机器代码。
2. Code That Can Benefit From Vectorization
如果代码对数组的许多连续元素执行相同的操作,则可以将其转换为向量化指令。例子:
float[] a = ...
for (int i = 0; i < a.length; i++) {
a[i] = a[i] * a[i];
}
3. 检查是否使用到向量化
3.1. Prepare a Mirco Benchmark
想要看生成的向量指令汇编代码,我们首先必须创建一个可以从向量指令中受益的可编译和可运行的 Java 程序。为此,将上述 for 循环放入 Java 文件中,放入 square(…) 方法以及 main(…) 方法。编写代码,使 square(…) 执行一百万次,或至少几十万次。这使编译器相信 square(…) 是一种值得最优化的方法。然后称 square(…) 为“热运行”或包含“热循环”。这种热运行是通过 main(…) 中的 for 循环实现的。所以我们有两个循环,一个在 main(…) 中,一个在 square(…) 中。热循环是 square(…)中的循环。
/**
* Run with this command to show native assembly:<br/>
* Java -XX:+UnlockDiagnosticVMOptions
* -XX:CompileCommand=print,VectorizationMicroBenchmark.square
* VectorizationMicroBenchmark
*/
public class VectorizationMicroBenchmark {
private static void square(float[] a) {
for (int i = 0; i < a.length; i++) {
a[i] = a[i] * a[i]; // line 11
}
}
public static void main(String[] args) throws Exception {
float[] a = new float[1024];
// repeatedly invoke the method under test. this
// causes the JIT compiler to optimize the method
for (int i = 0; i < 1000 * 1000; i++) {
square(a);
}
}
}
3.2 linux 命令行运行输出汇编指令
前提条件:需要将 hsdis-amd64.so 放到 /usr/lib/jvm/java-11/lib/server 目录
3.2.1 安装 hsdis-amd64.so
git clone https://github.com/liuzhengyang/hsdis
cd hsdis
tar -zxvf binutils-2.26.tar.gz
make BINUTILS=binutils-2.26 ARCH=amd64
cp build/linux-amd64/hsdis-amd64.so /usr/lib/jvm/java-11/lib/server
3.2.2 运行输出汇编
java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,VecOpMicroBenchmark.profile -XX:+PrintAssembly VectorizationMicroBenchmark.java > VectorizationMicroBenchmark.s
查看 VectorizationMicroBenchmark.s 中的汇编指令,找到 square 函数对应的指令,可以看到使用了 intel 向量化指令 vmulps 。