Java 自动矢量化示例

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)

为了确定它是否被矢量化,我做了以下事情:

  1. 打开eclipse并创建上面的文件
  2. 右键单击该文件,然后从下拉菜单中选择“运行”>“Java 应用程序”(暂时忽略输出)
  3. 在 eclipse 菜单中,单击Run > Run Configurations...
  4. 在打开的窗口中,找到 VecOpMicroBenchmark,单击它并选择Arguments 选项卡
  5. 在参数选项卡中,在VM 参数下:输入:-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,VecOpMicroBenchmark.profile
  6. 获取 libhsdis并将文件hsdis-amd64.so(Windows 的 .dll )复制(可能重命名)到 java/lib 目录。就我而言,这是/usr/lib/jvm/java-11-openjdk-amd64/lib.
  7. 再次运行 VecOpMicroBenchmark

它现在应该向控制台打印大量信息,其中一部分是由 JIT 编译器生成的反汇编的本地机器代码。如果您看到很多消息,但没有像movpushadd等那样的汇编指令,那么也许您可以在某处找到以下消息: 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位,即floatdouble)。
我在这里期望的是vaddps,这意味着添加打包的单精度浮点值并且这是一条真正的 SIMD 指令(SIMD = 单指令,多数据 = 向量化指令)。这里,packed意味着多个浮点数打包在一个 register 中

关于 ..ss 和 ..ps,见http://www.songho.ca/misc/sse/sse.html

SSE 定义了两种类型的操作;标量和打包。标量运算仅对最低有效数据元素(位 0~31)进行运算,而打包运算则并行计算所有四个元素。SSE 指令有一个后缀 -ss 表示标量操作(单一标量)和 -ps 表示打包操作(并行标量)。

问题:
我的 java 示例是否不正确,或者为什么输出中没有 SIMD 指令?

Dan*_* S. 8

在该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 中的自动矢量化