Java 自动矢量化示例

Java auto vectorization example

我试图找到一个简洁的示例,它在 x86-64 系统上的 java 中显示 auto vectorization

我在 for 循环中使用 y[i] = y[i] + x[i] 实现了以下代码。这段代码可以受益于自动矢量化,所以我认为 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");
        }
    }

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

  1. 打开eclipse并创建上面的文件
  2. 右键单击该文件,然后从下拉菜单中选择 运行 > Java Application(暂时忽略输出)
  3. 在 eclipse 菜单中,单击 运行 > 运行 配置...
  4. 在打开的window中,找到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 用于反汇编本地机器码以使其可读。您可以在 How to see JIT-compiled code in JVM? 找到更多关于如何 get/install 它的信息。

在输出中找到汇编指令后,我浏览了它(太多了 post 这里的所有内容)并寻找 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)

所以它使用的是 AVX 指令 vaddss。但是,如果我在这里是正确的,vaddss 意味着 添加标量单精度浮点值,这只会将一个浮点值添加到另一个浮点值(这里,标量意味着只是one,而这里 single 表示 32 位,即 float 而不是 double).
我在这里期望的是 vaddps,这意味着 add packed single-precision floating-point values 并且这是一个真正的 SIMD 指令(SIMD = 单指令,多数据 = 向量化指令).这里,packed 表示多个浮点数打包在一个寄存器中

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

SSE defines two types of operations; scalar and packed. Scalar operation only operates on the least-significant data element (bit 0~31), and packed operation computes all four elements in parallel. SSE instructions have a suffix -ss for scalar operations (Single Scalar) and -ps for packed operations (Parallel Scalar).

问题:
我的 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)

问题中的代码实际上可以由 JIT 编译器使用自动矢量化进行优化。然而,正如 Peter Cordes 在评论中指出的那样,JIT 需要相当多的处理,因此它不太愿意决定它应该完全优化一些代码。
解决方法很简单,就是在程序执行一次的过程中更频繁地执行代码,不只是 1000 次,而是 100000 次或 100 万次。
当多次执行 profile() 方法时,JIT 编译器确信代码非常重要,整体运行时将受益于全面优化,因此它再次优化代码,然后它也使用真正的向量指令,如 vaddps.

Auto Vectorization in Java

中有更多详细信息