首页 文章

任何JVM的JIT编译器都会生成使用向量化浮点指令的代码吗?

提问于
浏览
87

假设我的Java程序的瓶颈确实是计算一堆矢量点积的一些紧密循环 . 是的我已经分析过,是的,它是瓶颈,是的,它是重要的,是的,这就是算法的方式,是的,我运行Proguard来优化字节码等 .

这项工作基本上是点产品 . 在,我有两个 float[50] ,我需要计算成对产品的总和 . 我知道存在处理器指令集以快速和批量地执行这些操作,如SSE或MMX .

是的我可以通过在JNI中编写一些本机代码来访问它们 . JNI电话非常昂贵 .

我知道你无法保证JIT编译或编译的内容 . 有没有人听说过使用这些指令的JIT生成代码?如果有的话,有什么关于Java代码可以帮助它以这种方式编译吗?

可能是“不”;值得一提 .

8 回答

  • 3

    所以,基本上,您希望代码运行得更快 . JNI就是答案 . 我知道你说它不适合你,但让我告诉你,你错了 .

    这是 Dot.java

    import java.nio.FloatBuffer;
    import org.bytedeco.javacpp.*;
    import org.bytedeco.javacpp.annotation.*;
    
    @Platform(include="Dot.h", compiler="fastfpu")
    public class Dot {
        static { Loader.load(); }
    
        static float[] a = new float[50], b = new float[50];
        static float dot() {
            float sum = 0;
            for (int i = 0; i < 50; i++) {
                sum += a[i]*b[i];
            }
            return sum;
        }
        static native @MemberGetter FloatPointer ac();
        static native @MemberGetter FloatPointer bc();
        static native float dotc();
    
        public static void main(String[] args) {
            FloatBuffer ab = ac().capacity(50).asBuffer();
            FloatBuffer bb = bc().capacity(50).asBuffer();
    
            for (int i = 0; i < 10000000; i++) {
                a[i%50] = b[i%50] = dot();
                float sum = dotc();
                ab.put(i%50, sum);
                bb.put(i%50, sum);
            }
            long t1 = System.nanoTime();
            for (int i = 0; i < 10000000; i++) {
                a[i%50] = b[i%50] = dot();
            }
            long t2 = System.nanoTime();
            for (int i = 0; i < 10000000; i++) {
                float sum = dotc();
                ab.put(i%50, sum);
                bb.put(i%50, sum);
            }
            long t3 = System.nanoTime();
            System.out.println("dot(): " + (t2 - t1)/10000000 + " ns");
            System.out.println("dotc(): "  + (t3 - t2)/10000000 + " ns");
        }
    }
    

    Dot.h

    float ac[50], bc[50];
    
    inline float dotc() {
        float sum = 0;
        for (int i = 0; i < 50; i++) {
            sum += ac[i]*bc[i];
        }
        return sum;
    }
    

    我们可以使用命令行编译和运行JavaCPP

    $ javac -cp javacpp.jar Dot.java
    $ java -jar javacpp.jar Dot
    $ java -cp javacpp.jar:. Dot
    

    使用英特尔酷睿i7-3632QM CPU @ 2.20GHz,Fedora 20,GCC 4.8.3和OpenJDK 7或8,我得到这种输出:

    dot(): 37 ns
    dotc(): 23 ns
    

    或大约快1.6倍 . 我们需要使用直接NIO缓冲区而不是数组,但是HotSpot can access direct NIO buffers as fast as arrays . 另一方面,在这种情况下,手动展开循环不会提供可测量的性能提升 .

  • 35

    为了解决其他人在这里表达的一些怀疑,我建议任何想要证明自己或其他人使用以下方法的人:

    • 创建一个JMH项目

    • 写一小段可矢量数学 .

    • 在-XX:-UseSuperWord和-XX之间运行基准翻转:UseSuperWord(默认)

    • 如果没有观察到性能差异,您的代码可能无法进行矢量化

    • 为了确保,运行您的基准测试,以便打印出装配体 . 在linux上你可以享受perfasm分析器('-prof perfasm')看一看,看看你期望的指令是否生成 .

    例:

    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE) //makes looking at assembly easier
    public void inc() {
        for (int i=0;i<a.length;i++)
            a[i]++;// a is an int[], I benchmarked with size 32K
    }
    

    带有和不带标志的结果(最近的Haswell笔记本电脑,Oracle JDK 8u60): - XX:UseSuperWord:475.073±44.579 ns / op(每个操作纳秒)-XX:-UseSuperWord:3376.364±233.211 ns / op

    热循环的程序集有点格式化并坚持在这里,但这里是一个片段(hsdis.so无法格式化一些AVX2向量指令,所以我运行-XX:UseAVX = 1): - XX:UseSuperWord ('-prof perfasm:intelSyntax = true')

    9.15%   10.90%  │││ │↗    0x00007fc09d1ece60: vmovdqu xmm1,XMMWORD PTR [r10+r9*4+0x18]
     10.63%    9.78%  │││ ││    0x00007fc09d1ece67: vpaddd xmm1,xmm1,xmm0
     12.47%   12.67%  │││ ││    0x00007fc09d1ece6b: movsxd r11,r9d
      8.54%    7.82%  │││ ││    0x00007fc09d1ece6e: vmovdqu xmm2,XMMWORD PTR [r10+r11*4+0x28]
                      │││ ││                                                  ;*iaload
                      │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@17 (line 45)
     10.68%   10.36%  │││ ││    0x00007fc09d1ece75: vmovdqu XMMWORD PTR [r10+r9*4+0x18],xmm1
     10.65%   10.44%  │││ ││    0x00007fc09d1ece7c: vpaddd xmm1,xmm2,xmm0
     10.11%   11.94%  │││ ││    0x00007fc09d1ece80: vmovdqu XMMWORD PTR [r10+r11*4+0x28],xmm1
                      │││ ││                                                  ;*iastore
                      │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@20 (line 45)
     11.19%   12.65%  │││ ││    0x00007fc09d1ece87: add    r9d,0x8            ;*iinc
                      │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@21 (line 44)
      8.38%    9.50%  │││ ││    0x00007fc09d1ece8b: cmp    r9d,ecx
                      │││ │╰    0x00007fc09d1ece8e: jl     0x00007fc09d1ece60  ;*if_icmpge
    

    玩得开心冲进城堡!

  • 5

    在以Java 7u40开头的HotSpot版本中,服务器编译器提供对自动矢量化的支持 . 根据JDK-6340864

    然而,这似乎只适用于"simple loops" - 至少目前是这样 . 例如,累积数组无法进行矢量化JDK-7192383

  • 3

    这篇文章是关于试验我的朋友写的Java和SIMD指令的好文章:http://prestodb.rocks/code/simd/

    它的一般结果是你可以期望JIT在1.8中使用一些SSE操作(以及1.9中的一些操作) . 虽然你不应该期待太多,但你需要小心 .

  • 25

    您可以编写OpenCl内核来进行计算并从java http://www.jocl.org/运行它 .

    代码可以在CPU和/或GPU上运行,OpenCL语言也支持矢量类型,因此您应该能够明确地利用例如SSE3 / 4指令 .

  • 4

    我猜你在发现netlib-java之前写了这个问题;-)它提供了你需要的本机API,机器优化的实现,并且由于内存固定而在原生边界没有任何成本 .

  • 39

    看看Performance comparison between Java and JNI for optimal implementation of computational micro-kernels . 他们表明Java HotSpot VM服务器编译器支持使用超级字级并行的自动矢量化,这仅限于循环并行内部的简单情况 . 本文还将为您提供一些指导,指出您的数据大小是否足以证明JNI路由的合理性 .

  • -3

    我不相信大多数虚拟机是否足够智能以进行这种优化 . 公平地说,大多数优化都要简单得多,比如在2的幂时移位而不是乘法 . mono项目引入了他们自己的矢量和其他方法与原生支持,以帮助提高性能 .

相关问题