java 向量计算 jvm向量化 | 您所在的位置:网站首页 › 向量化执行 › java 向量计算 jvm向量化 |
摘要 这篇文章简单解释了向量化的目的,它是如何在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 xmm01.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: * 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 tabin 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原理 – 孙希栋的博客 c++ SIMD 样例 SIMD指令初学 |
CopyRight 2018-2019 实验室设备网 版权所有 |