【问题标题】:Do any JVM's JIT compilers generate code that uses vectorized floating point instructions?是否有任何 JVM 的 JIT 编译器生成使用矢量化浮点指令的代码?
【发布时间】:2012-06-02 20:07:38
【问题描述】:

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

这项工作本质上是点积。如,我有两个float[50],我需要计算成对产品的总和。我知道处理器指令集的存在是为了快速批量执行此类操作,例如 SSE 或 MMX。

是的,我可以通过在 JNI 中编写一些本机代码来访问这些。事实证明,JNI 调用非常昂贵。

我知道你不能保证 JIT 会编译什么,什么不编译。有没有人曾经听说过使用这些指令的 JIT 生成代码?如果是这样,Java 代码有什么东西可以帮助它以这种方式编译吗?

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

【问题讨论】:

标签: java floating-point jit sse vectorization


【解决方案1】:

所以,基本上,您希望您的代码运行得更快。 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

使用 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz、Fedora 30、GCC 9.1.1 和 OpenJDK 8 或 11,我得到这样的输出:

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

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

【讨论】:

  • 您使用的是 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,混合模式)
  • 那个循环可能有一个循环依赖。通过展开循环两次或更多次,您可能会获得进一步的加速。
  • @Oliv GCC 用 SSE 向量化代码,是的,但是对于这么小的数据,不幸的是 JNI 调用开销太大了。
  • 在我的带有 JDK 13 的 A6-7310 上,我得到:dot(): 69 ns / dotc(): 95 ns。 Java 赢了!
【解决方案2】:

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

  • 创建 JMH 项目
  • 写一个向量化数学的小sn-p。
  • 在 -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

热循环的程序集需要格式化和粘贴在这里,但这里有一个 sn-p(hsdis.so 无法格式化一些 AVX2 向量指令,所以我使用 -XX:UseAVX=1 运行) : -XX:+UseSuperWord(with '-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

玩得开心冲进城堡!

【讨论】:

  • 来自同一篇论文:“JITed 反汇编程序输出表明,在调用最佳 SIMD 指令及其调度方面实际上效率不高。快速搜索 JVM JIT 编译器 (Hotspot)源代码表明这是由于不存在打包的 SIMD 指令代码。” SSE 寄存器在标量模式下使用。
  • @AleksandrDubinsky 有些案例包括在内,有些则没有。您有感兴趣的具体案例吗?
  • 让我们翻转这个问题,问一下JVM是否会自动向量化任何算术运算?你能举个例子吗?我确实有一个循环,最近我不得不使用内在函数将其拉出并重写。然而,与其希望自动矢量化,我更希望看到对显式矢量化/内在函数的支持(类似于agner.org/optimize/vectorclass.pdf)。更好的是为 Aparapi 编写一个好的 Java 后端(尽管该项目的领导有一些错误的目标)。你在 JVM 上工作吗?
  • @AleksandrDubinsky 我希望扩展的答案会有所帮助,如果不是电子邮件的话。另请注意,“使用内在函数重写”意味着您更改了 JVM 代码以添加新的内在函数,这就是您的意思吗?我猜你的意思是用通过 JNI 调用本机实现来替换你的 Java 代码
  • 谢谢。现在应该是官方的回答了。我认为您应该删除对论文的引用,因为它已经过时并且没有演示矢量化。
【解决方案3】:

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

然而,这似乎只适用于“简单循环”——至少目前如此。比如累加一个数组还不能向量化JDK-7192383

【讨论】:

  • 在某些情况下,JDK6 中也有向量化,尽管目标 SIMD 指令集没有那么宽。
  • 由于英特尔的贡献,HotSpot 中的编译器矢量化支持最近(2017 年 6 月)得到了很大改进。由于启用了 AVX2 的错误修复,尚未发布的 jdk9(b163 及更高版本)目前在性能方面胜过 jdk8。循环必须满足一些约束才能使自动矢量化工作,例如使用:int 计数器,常量计数器增量,一个带有循环不变变量的终止条件,没有方法调用的循环体(?),没有手动循环展开!详情可见:cr.openjdk.java.net/~vlivanov/talks/…
  • 矢量化融合多重加法 (FMA) 支持目前看起来不太好(截至 2017 年 6 月):它是矢量化或标量 FMA(?)。然而,甲骨文显然刚刚接受了英特尔对 HotSpot 的贡献,该热点支持使用 AVX-512 实现 FMA 矢量化。让自动矢量化爱好者和那些幸运的能够访问 AVX-512 硬件的人感到高兴的是,这可能会(运气好的话)出现在下一个 jdk9 EA 版本(b175 之外)中。
  • 支持上一条语句的链接(RFR(M): 8181616: FMA Vectorization on x86):mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
  • 一个小型基准测试,通过使用 AVX2 指令的循环向量化来展示整数 4 倍的加速:prestodb.rocks/code/simd
【解决方案4】:

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

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

【讨论】:

  • 如果您总结了您链接到的文章的一些关键见解,将会有所帮助。
【解决方案5】:

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

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

【讨论】:

    【解决方案6】:

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

    【讨论】:

      【解决方案7】:

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

      【讨论】:

      • 是的,很久以前。我更希望听到这会自动转换为矢量化指令。但显然,手动实现并不难。
      【解决方案8】:

      Java 16 引入了 Vector API(JEP 417JEP 414JEP 338)。它目前正在“孵化”(即测试版),尽管任何人都可以使用它。它可能会在 Java 19 或 20 中成为 GA。

      它有点冗长,但应该是可靠和可移植的。

      以下代码可以改写:

      void scalarComputation(float[] a, float[] b, float[] c) {
         assert a.length == b.length && b.length == c.length;
         for (int i = 0; i < a.length; i++) {
              c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
         }
      }
      

      使用矢量 API:

      static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
      
      void vectorComputation(float[] a, float[] b, float[] c) {
          assert a.length == b.length && b.length == c.length;
          int i = 0;
          int upperBound = SPECIES.loopBound(a.length);
          for (; i < upperBound; i += SPECIES.length()) {
              // FloatVector va, vb, vc;
              var va = FloatVector.fromArray(SPECIES, a, i);
              var vb = FloatVector.fromArray(SPECIES, b, i);
              var vc = va.mul(va)
                         .add(vb.mul(vb))
                         .neg();
              vc.intoArray(c, i);
          }
          for (; i < a.length; i++) {
              c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
          }
      }
      

      较新的版本(即 Java 18)正在尝试使用谓词指令摆脱最后一个 for 循环,但据推测对此的支持仍然参差不齐。

      【讨论】:

        【解决方案9】:

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

        【讨论】:

        • 目前,没有 Java 热点编译器能做到这一点,但这并不比他们所做的事情难多少。他们确实使用 SIMD 指令一次复制多个数组值。您只需要编写更多的模式匹配和代码生成代码,在进行一些循环展开之后就非常简单了。我认为 Sun 的人只是变得懒惰了,但现在 Oracle 似乎会发生这种情况(耶弗拉基米尔!这应该对我们的代码有很大帮助!):mail.openjdk.java.net/pipermail/hotspot-compiler-dev/…
        猜你喜欢
        • 2019-05-10
        • 1970-01-01
        • 1970-01-01
        • 2011-02-04
        • 1970-01-01
        • 2017-06-09
        • 2016-06-23
        • 1970-01-01
        • 2010-12-02
        相关资源
        最近更新 更多