JVM的JIT编译器是否生成使用矢量化浮点指令的代码?


95

可以说,我的Java程序的瓶颈实际上是一些紧密的循环,无法计算一堆矢量点积。是的,我已经进行了概要分析,是的,它是瓶颈,是的,它很重要,是的,这就是算法的方式,是的,我已经运行了Proguard来优化字节码,等等。

实质上,这是点产品。float[50]与之类似,我有两个,我需要计算成对乘积之和。我知道处理器指令集可以像SSE或MMX一样快速且批量地执行此类操作。

是的,我可能可以通过在JNI中编写一些本机代码来访问它们。JNI调用结果非常昂贵。

我知道您不能保证JIT将编译或不编译。有没有人曾经听说过使用这些指令的JIT生成的代码?如果是这样,那么有关Java代码的任何内容都可以使它以这种方式编译吗?

可能是“否”;值得一问。


4
最简单的查找方法可能是获取您可以找到的最现代的JIT,并使用来输出生成的程序集-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation。您将需要一个程序,该程序必须运行足够多的时间才能使其变为矢量。
Louis Wasserman



3
实际上,根据此博客,如果“正确”使用JNI可以相当快。
ziggystar

2
有关此问题的相关博客文章,请参见:psy-lob-saw.blogspot.com/2015/04/…,其中包含矢量化可能发生并且确实发生的一般信息。除了对特定情况进行向量化(Arrays.fill()/ equals(char [])/ arrayCopy)之外,JVM还使用超字级并行化对向量进行自动向量化。相关代码在superword.cpp中,其基于的文件在此处:groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart 2015年

Answers:


44

因此,基本上,您希望代码运行得更快。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 @NoException 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进行编译和运行:

$ java -jar javacpp.jar Dot.java -exec

使用2.80GHz @ Fedora 30,GCC 9.1.1和OpenJDK 8或11的Intel®Core™i7-7700HQ CPU,可以获得以下输出:

dot(): 39 ns
dotc(): 16 ns

或大约快2.4倍。我们需要使用直接NIO缓冲区而不是数组,但是HotSpot可以像访问数组一样快地访问直接NIO缓冲区。另一方面,在这种情况下,手动展开循环不会显着提高性能。


3
您使用的是OpenJDK还是Oracle HotSpot?与普遍的看法相反,它们并不相同。
乔纳森·费舍尔

@exabrial这是“ java -version”现在在此计算机上返回的内容:java版本“ 1.6.0_22” OpenJDK运行时环境(IcedTea6 1.10.6)(fedora-63.1.10.6.fc15-x86_64)OpenJDK 64位服务器VM (版本20.0-b11,混合模式)
塞缪尔·奥德

1
该循环可能具有带进位的循环依赖性。通过将循环展开两次或更多次,可以进一步提高速度。

3
@Oliv GCC使用SSE对代码进行矢量化处理,是的,但是对于这么小的数据,不幸的是JNI调用开销太大。
塞缪尔·奥德

2
在具有JDK 13的A6-7310上,我得到:dot():69 ns / dotc():95 ns。Java胜了!
Stefan Reich

39

为了解决其他人对此表示的怀疑,我建议任何想证明自己或其他人的人都使用以下方法:

  • 创建一个JMH项目
  • 编写一小段可矢量化的数学代码。
  • 在-XX:-UseSuperWord和-XX:+ UseSuperWord(默认)之间运行基准转换
  • 如果未观察到性能差异,则您的代码可能没有被矢量化
  • 为确保确定,请运行基准测试,以使其打印出程序集。在Linux上,您可以欣赏一下perfasm profiler('-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

快来冲城堡吧!


1
在同一篇文章中:“ JITed反汇编程序的输出表明,在调用最优化的SIMD指令及其调度方面实际上并没有那么有效。快速浏览JVM JIT编译器(Hotspot)源代码表明,这是由于打包的SIMD指令代码不存在。” SSE寄存器用于标量模式。
Aleksandr Dubinsky

1
@AleksandrDubinsky某些案例涉及,有些则没有。您是否有感兴趣的具体案例?
Nitsan Wakart

2
让我们翻转问题,并询问JVM是否将自动对任何算术运算进行矢量化处理?你能举个例子吗?我确实有一个循环,最近我不得不退出并使用内在函数进行重写。但是,我希望看到显式矢量化/内部支持(类似于agner.org/optimize/vectorclass.pdf),而不是希望实现自动矢量化。更好的办法是为Aparapi编写一个好的Java后端(尽管该项目的领导者有一些错误的目标)。您是否在JVM上工作?
Aleksandr Dubinsky

1
@AleksandrDubinsky我希望扩展的答案对您有所帮助,即使不是一封电子邮件也可以。还要注意,“使用内部函数重写”意味着您更改了JVM代码以添加新的内部函数,这是您的意思吗?我猜想你的意思是通过JNI对本地实现的调用替换Java代码
Nitsan Wakart 2015年

1
谢谢。现在这应该是官方的答案。我认为您应该删除对本文的引用,因为它已经过时并且没有演示矢量化。
Aleksandr Dubinsky 2015年

26

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

但是,这似乎仅对“简单循环”有效-至少目前如此。例如,累积数组无法向量化,但JDK-7192383


在某些情况下,尽管目标SIMD指令集没有那么宽泛,但在某些情况下,JDK6中也存在矢量化。
尼桑·瓦卡特

3
由于英特尔的贡献,HotSpot中的编译器矢量化支持最近(2017年6月)得到了很大改进。在性能方面,由于启用AVX2的错误修复,尚未发布的jdk9(b163和更高版本)目前胜过jdk8。循环必须满足一些约束才能使自动矢量化工作,例如,使用:int计数器,恒定的计数器增量,一个带有循环不变变量的终止条件,没有方法调用(?)的循环主体,没有手动循环展开!有关详细信息,请访问:cr.openjdk.java.net/~vlivanov/talks/…–
Vedran

向量化的融合多重加法(FMA)支持当前看起来不佳(截至2017年6月):它是向量化或标量FMA(?)。但是,Oracle显然刚刚接受了英特尔对HotSpot的贡献,该贡献使AVX-512能够实现FMA矢量化。自动矢量化迷和那些幸运的人可以使用AVX-512硬件感到高兴,这可能(有些运气)出现在下一个jdk9 EA版本(b175之后)中。
Vedran

支持上一条语句的链接(RFR(M):8181616:x86上的FMA矢量化):mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…–
Vedran

2
一个小型基准测试,使用AVX2指令通过循环矢量化将整数加速了4倍:prestodb.rocks/code/simd
Vedran

6

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

其一般结果是,您可以期望JIT在1.8中使用某些SSE操作(在1.9中使用更多)。尽管您期望不高,但是需要小心。


1
如果您总结了所链接文章的一些关键见解,将会很有帮助。
Aleksandr Dubinsky



3

我猜想您是在发现netlib-java之前写过这个问题的;-)它提供了您所需的本机API,并且具有机器优化的实现,并且由于内存固定,因此在本机边界没有任何成本。


1
是的,很久以前。我更希望听到将其自动翻译为矢量化指令的信息。但是很显然,手动进行操作并不难。
肖恩·欧文

-4

我不相信大多数VM是否足够聪明地进行这种优化。公平地讲,大多数优化要简单得多,例如当幂为2时,移位而不是乘法。mono项目引入了自己的向量以及其他具有本机支持的方法来帮助提高性能。


3
当前,没有Java热点编译器可以执行此操作,但是并没有比他们执行的操作难得多。他们确实使用SIMD指令一次复制多个数组值。您只需要编写更多的模式匹配和代码生成代码,在进行一些循环展开之后,这非常简单。我认为Sun的人们已经变得懒惰了,但是现在看来它将在Oracle发生(是的,弗拉基米尔!这应该对我们的代码有很大帮助!):mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Christopher Manning 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.