Dan*_* S. 6 java assembly x86-64 simd vectorization
我试图找到一个简洁的例子,它显示了 x86-64 系统上 java中的自动矢量化。
我已经y[i] = y[i] + x[i]
在 for 循环中使用了以下代码。这段代码可以从自动向量化中受益,所以我认为 java 应该在运行时使用 SSE 或 AVX 指令编译它以加快速度。
但是,我在生成的本机机器代码中找不到矢量化指令。
VecOpMicroBenchmark.java
应该受益于自动矢量化:
/**
* Run with this command to show native assembly:<br/>
* java -XX:+UnlockDiagnosticVMOptions
* -XX:CompileCommand=print,VecOpMicroBenchmark.profile VecOpMicroBenchmark
*/
public class VecOpMicroBenchmark {
private static final int LENGTH = 1024;
private static long profile(float[] x, float[] y) {
long t = System.nanoTime();
for (int i = 0; i < LENGTH; i++) {
y[i] = y[i] + x[i]; // line 14
}
t = System.nanoTime() - t;
return t;
}
public static void main(String[] args) throws Exception {
float[] x = new float[LENGTH];
float[] y = new float[LENGTH];
// to let the JIT compiler do its work, repeatedly invoke
// the method under test and then do a little nap
long minDuration = Long.MAX_VALUE;
for (int i = 0; i < 1000; i++) {
long duration = profile(x, y);
minDuration = Math.min(minDuration, duration);
}
Thread.sleep(10);
System.out.println("\n\nduration: " + minDuration + "ns");
}
}
Run Code Online (Sandbox Code Playgroud)
为了确定它是否被矢量化,我做了以下事情:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,VecOpMicroBenchmark.profile
hsdis-amd64.so
(Windows 的 .dll )复制(可能重命名)到 java/lib 目录。就我而言,这是/usr/lib/jvm/java-11-openjdk-amd64/lib
.它现在应该向控制台打印大量信息,其中一部分是由 JIT 编译器生成的反汇编的本地机器代码。如果您看到很多消息,但没有像mov
、push
、add
等那样的汇编指令,那么也许您可以在某处找到以下消息:
Could not load hsdis-amd64.so; library not loadable; PrintAssembly is disabled
这意味着 java 找不到该文件hsdis-amd64.so
- 它不在正确的目录中或没有正确的名字。
hsdis-amd64.so
是显示生成的本地机器代码所需的反汇编程序。JIT编译器将java字节码编译成原生机器码后,hsdis-amd64.so
用于反汇编原生机器码,使其可读。您可以在如何在 JVM 中查看 JIT 编译的代码中找到有关如何获取/安装它的更多信息?.
在输出中找到汇编指令后,我浏览了它(太多了,无法在此处发布所有内容)并查找line 14
. 我找到了这个:
0x00007fac90ee9859: nopl 0x0(%rax)
0x00007fac90ee9860: cmp 0xc(%rdx),%esi ; implicit exception: dispatches to 0x00007fac90ee997f
0x00007fac90ee9863: jnb 0x7fac90ee9989
0x00007fac90ee9869: movsxd %esi,%rbx
0x00007fac90ee986c: vmovss 0x10(%rdx,%rbx,4),%xmm0 ;*faload {reexecute=0 rethrow=0 return_oop=0}
; - VecOpMicroBenchmark::profile@16 (line 14)
0x00007fac90ee9872: cmp 0xc(%rdi),%esi ; implicit exception: dispatches to 0x00007fac90ee9997
0x00007fac90ee9875: jnb 0x7fac90ee99a1
0x00007fac90ee987b: movsxd %esi,%rbx
0x00007fac90ee987e: vmovss 0x10(%rdi,%rbx,4),%xmm1 ;*faload {reexecute=0 rethrow=0 return_oop=0}
; - VecOpMicroBenchmark::profile@20 (line 14)
0x00007fac90ee9884: vaddss %xmm1,%xmm0,%xmm0
0x00007fac90ee9888: movsxd %esi,%rbx
0x00007fac90ee988b: vmovss %xmm0,0x10(%rdx,%rbx,4) ;*fastore {reexecute=0 rethrow=0 return_oop=0}
; - VecOpMicroBenchmark::profile@22 (line 14)
Run Code Online (Sandbox Code Playgroud)
所以它使用 AVX 指令vaddss
。但是,如果我是正确的在这里,vaddss
手段
加标量单精度浮点值,这只是增加了一个浮点值到另外一个(在这里,标量的手段只有一个,而在这里一个手段32位,即float
不double
)。
我在这里期望的是vaddps
,这意味着添加打包的单精度浮点值并且这是一条真正的 SIMD 指令(SIMD = 单指令,多数据 = 向量化指令)。这里,packed意味着多个浮点数打包在一个 register 中。
关于 ..ss 和 ..ps,见http://www.songho.ca/misc/sse/sse.html:
SSE 定义了两种类型的操作;标量和打包。标量运算仅对最低有效数据元素(位 0~31)进行运算,而打包运算则并行计算所有四个元素。SSE 指令有一个后缀 -ss 表示标量操作(单一标量)和 -ps 表示打包操作(并行标量)。
问题:
我的 java 示例是否不正确,或者为什么输出中没有 SIMD 指令?
在该main()
方法中,放入i < 1000000
而不只是i < 1000
. 然后 JIT 还会生成如下所示的 AVX 向量指令,并且代码运行速度更快:
0x00007f20c83da588: vmovdqu 0x10(%rbx,%r11,4),%ymm0
0x00007f20c83da58f: vaddps 0x10(%r13,%r11,4),%ymm0,%ymm0
0x00007f20c83da596: vmovdqu %ymm0,0x10(%rbx,%r11,4) ;*fastore {reexecute=0 rethrow=0 return_oop=0}
; - VecOpMicroBenchmark::profile@22 (line 14)
Run Code Online (Sandbox Code Playgroud)
问题中的代码实际上可以由 JIT 编译器使用自动向量化进行优化。然而,正如 Peter Cordes 在评论中指出的那样,JIT 需要相当多的处理,因此它不太愿意决定应该完全优化某些代码。
解决方案很简单,就是在程序的一次执行过程中更频繁地执行代码,不仅仅是1000次,而是100000次或100万次。
当多次执行profile()
该方法时,JIT编译器确信该代码非常重要,并且整体运行时将受益于全面优化,因此它再次优化代码,然后它还使用真正的向量指令,例如vaddps
.
更多详细信息,请参阅Java 中的自动矢量化