摘要
这篇文章简单解释了向量化的目的,它是如何在Java中生效的,如何去检测它是否被应用于Java程序中。这些知识对数学计算大有帮助。
这些方式比较底层,且只适用于特殊的场景。如果你想优化你的Java程序性能。你的首选应是其他可用的优化方法。只有当您已经使用其他技术优化了Java代码,并在之后对其进行了分析,并且得出结论,专注于算术计算的部分可能会通过并行化运行得更快,那么本文可能对您有用。
1.简介
1.1 SIMD
通常程序代码是被串行执行的。计算型程序通常需要在数据上做大量计算。一般情况下多数数据需要被相同的方式处理。例如,对于1000个粒子的模拟,模拟中会有一个步骤,用其当前速度更新每个粒子的位置 v: 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];
也就是说在多个数据上一次性执行一个操作。 在汇编代码级别,有专门针对这些分组操作的说明。此概念被叫做single instruction, multiple data简称SIMD。
对于+操作来说SIMD指令的被称作addps(SSE instruction set) 或者vaddps(AVX instruction set) 在X86 架构上。它使用两组数据作为操作数。每组4 (SSE instruction set)或8个 (AVX instruction set)数据。 对分组中的每个数据进行加法操作。对于上面的例子来说 s[0..3]是一组
v[0..3]是另一组,x86的汇编代码如下
addps %xmm0,%xmm1 ;add vector in xmm0 to vector in xmm1, store result in xmm0
1.2 向量化
SIMD是从指令设计者(即CPU制造商)的角度给出的概念名称。但这不是唯一的观点。在数学上,由固定数量的元素组成的有序组(s[0..3]
and v[0..3]
) 被称作向量。所以SIMD也被叫做向量指令。这只是对同一件事的另一个观点,是从使用指令的用户观点来看的。
矢量化是使用矢量指令来加速程序执行。矢量化可以由程序员完成,也可以由编译器自动实现。在后一种情况下,它被称为自动矢量化(auto vectorization)。
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 Java 中的向量指令
编写Java程序后,Java文件中的Java源代码被编译成字节码并保存到类文件中。然后,在程序执行之前或执行过程中,它的字节码通常会再次编译,这次是从字节码到本机机器码。后一种编译通常在程序执行时进行,因此它是JIT编译( JIT compilation)。
在Java中,目前矢量化不是由程序员完成的1,而是由编译器自动完成的。编译器接受标准Java字节码,并自动确定哪些部分可以转换为向量指令。像OpenJDK或Oracle的Java这样的通用Java环境可以生成矢量化的机器代码。
2. 可以从矢量化受益的代码
如果代码对数组的多个连续元素执行相同的操作,则可以将代码转换为矢量化指令。例如
float[] a = ...
for (int i = 0; i < a.length; i++) {
a[i] = a[i] * a[i];
}
如上,语句a[i] = a[i] * a[i] 在数组的多个连续元素上执行。编译器可以对此进行检查,而不是单独执行每个*操作,它可以使用向量指令一次计算多个结果。
3. 检查向量化是否被使用
3.1 准备一个Mirco基准
为了生成汇编级别的向量指令,我们首先需要创建一个可以从向量指令中获益且可以运行的java程序。 如下:
/**
* 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);
}
}
}
上面的程序可以使编译器把square进行优化。
3.2 运行Mirco基准
- Open Eclipse and create a new project. Create a new class in the new project and name it
VectorizationMicroBenchmark
. Copy-paste the code of Snippet 2 into it.- Right-click the file and from the dropdown menu, choose Run > Java Application (ignore the output for now)
- In Eclipse's menu, click Run > Run Configurations...
- A window opened. In the window, find VectorizationMicroBenchmark, click it and choose the Arguments tab
- in the Arguments tab, under VM arguments: put in this:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,VectorizationMicroBenchmark.square
- get libhsdis and copy (possibly rename) the file
hsdis-amd64.so
(.dll for windows) to your Java-home/lib directory. On Ubuntu, this is something like/usr/lib/jvm/Java-11-openjdk-amd64/lib
.- run VectorizationMicroBenchmark again
如果你没有Eclipse, 你可以在任何其他IDE中类似地执行这些步骤,或者在命令行上使用文本编辑器和javac/java。
第7步,将大量信息打印到控制台,其中一部分是反汇编的本机代码。如果您看到很多消息,但没有诸如mov、push、add等汇编指令,那么您可能可以在输出中的某个地方找到以下消息:Could not load hsdis-amd64.so; library not loadable; PrintAssembly is disabled
如果您看到此消息,这意味着Java找不到文件hsdis-amd64.so-它不在正确的目录中或没有正确的名称。在Linux上,创建符号链接时也会发生这种情况。Java在这里不接受符号链接。您必须复制该文件。hsdis-amd64.so是显示生成的本机代码所需的反汇编程序。JIT编译器将Java字节码编译为本机机器码后,hsdis-amd64.so用于反汇编本机机器码,使其具有可读性。您可以找到有关如何获取/安装它以及如何在JVM中查看JIT编译代码的更多信息(How to see JIT-compiled code in JVM.)。
3.3 输出说明
在输出中找到汇编指令后,您可能会惊讶地发现,您不仅找到了square(...)方法的汇编代码,而且你发现了它的多个版本。这是因为JIT编译器在第一次运行时没有完全优化该方法。在调用该方法之后,它将其编译为本机代码而不进行优化。在多次调用之后,它通过一些优化(但不是全部优化)再次编译该方法。只有在几千次调用之后,编译器才确信该方法非常重要,需要在编译时启用所有优化,包括向量化。因此,最好的编译通常是输出中的最后一个编译。
开始搜索输出末尾的“第11行”,向后搜索。您可能会发现如下内容:
0x...ac70: vmovss 0x10(%rbx,%rbp,4),%xmm0 ;*faload {reexecute=0 rethrow=0 return_oop=0}
; - VectorizationMicroBenchmark::square@9 (line 11)
0x...ac76: vmulss %xmm0,%xmm0,%xmm1
0x...ac7a: vmovss %xmm1,0x10(%rbx,%rbp,4) ;*fastore {reexecute=0 rethrow=0 return_oop=0}
; - VectorizationMicroBenchmark::square@14 (line 11)
请注意代码段3中末尾带有-ss的指令vmulss。
vmulss
: multiply scalar single-precision floating-point values
vmulss仅将一个浮点数与另一个浮点数相乘。所以这不是我们想要的。(这里,标量表示只有一个,单精度表示32位,即浮点而不是双精度)。相反,我们希望找到一条指令,它一次将许多浮点与许多其他浮点相乘。所以请继续关注。你最终会发现:
0x...ac54: vmovdqu 0x10(%rbx,%rbp,4),%ymm0 ;*faload {reexecute=0 rethrow=0 return_oop=0} ; - VectorizationMicroBenchmark::square@9 (line 11) 0x...ac5a: vmulps %ymm0,%ymm0,%ymm0 0x...ac5e: vmovdqu %ymm0,0x10(%rbx,%rbp,4) ;*fastore {reexecute=0 rethrow=0 return_oop=0} ; - VectorizationMicroBenchmark::square@14 (line 11)
vmulps
: multiply packed single-precision floating-point values
vmulps是一条真正的SIMD指令(提示:SIMD=单指令,多数据=矢量化指令)。这里,(packed )“打包”是指在一个寄存器中打包在一起的多个元素。这表明应用了自动矢量化。
4 引用
计划对Java进行扩展,这将允许程序员在源代码中显式使用向量指令。在编写本文时,这些扩展还没有准备好(Spring2020)。See JEP-338 and Project Panama: Interconnecting JVM and native code for details and status. (back)
5 参考文档
SSE - illustration of the results of -ps and -ss instructions:
x86 and amd64 instruction reference - x86 and amd64 instruction reference
http://jpbempel.blogspot.com/2015/12/printassembly-output-explained.html - PrintAssembly output explained
SIMD原理 – 孙希栋的博客https://www.sunxidong.com/357.html
c++ SIMD 样例_ACodeDog的博客-CSDN博客https://blog.csdn.net/weixin_41644391/article/details/113526563
SIMD指令初学_mick_seu的博客-CSDN博客_simd