web-dev-qa-db-ja.com

JVMのJITコンパイラは、ベクトル化された浮動小数点命令を使用するコードを生成しますか?

私のJavaプログラムのボトルネックは、実際には、多数のベクトルドット積を計算するためのタイトループです。アルゴリズムは、はい、Proguardを実行してバイトコードなどを最適化しました。

仕事は、本質的には内積です。同様に、2つのfloat[50]とペアワイズ積の合計を計算する必要があります。 SSEまたはMMXのような、これらの種類の操作を迅速かつ一括して実行するためのプロセッサ命令セットが存在することを知っています。

はい、おそらくJNIでネイティブコードを書くことでこれらにアクセスできます。 JNI呼び出しはかなり高価であることが判明しました。

JITがコンパイルするものとコンパイルしないものを保証できないことは知っています。誰かeverこれらの命令を使用するコードを生成するJITについて聞いたことがありますか?もしそうなら、この方法でコンパイル可能にするのに役立つJavaコードについて何かありますか?

おそらく「いいえ」。尋ねる価値があります。

89
Sean Owen

そのため、基本的には、コードをより速く実行したいでしょう。 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

Intel Core i7-3632QM CPU @ 2.20GHz、Fedora 20、GCC 4.8.3、およびOpenJDK 7または8では、次のような出力が得られます。

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

または約1.6倍高速です。配列の代わりに直接NIOバッファーを使用する必要がありますが、 HotSpotは配列と同じ速さで直接NIOバッファーにアクセスできます 。一方、この場合、ループを手動で展開しても、測定可能なパフォーマンスの向上は得られません。

41
Samuel Audet

ここで他の人によって表明された懐疑論のいくつかに対処するために、自分自身または他の人に証明したい人は誰でも次の方法を使用することをお勧めします:

  • 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(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

城を襲撃して楽しんでください!

37
Nitsan Wakart

Java 7u40で始まるHotSpotバージョンでは、サーバーコンパイラは自動ベクトル化のサポートを提供します。 JDK-6340864

ただし、これは「単純なループ」にのみ当てはまるようです-少なくとも現時点では。たとえば、配列の累積はまだベクトル化できません JDK-719238

26
Vedran

Javaと、友人が書いたSIMD命令の実験に関する素晴らしい記事です: http://prestodb.rocks/code/simd/

その一般的な結果は、JITが1.8(および1.9ではさらにいくつか)でSSE操作を使用することを期待できるということです。

5
kokosing

OpenClカーネルを記述して計算を実行し、Java http://www.jocl.org/ から実行できます。

コードはCPUおよび/またはGPUで実行でき、OpenCL言語はベクタータイプもサポートしているので、明示的にたとえばSSE3/4命令。

4
Mikael Lepistö

Netlib-Javaに気付く前にこの質問を書いたと思います;-)マシンに最適化された実装で、必要なネイティブAPIを正確に提供し、メモリ固定のおかげでネイティブ境界にコストがかかりません。

3
fommil

計算マイクロカーネルの最適な実装のためのJavaとJNIのパフォーマンス比較 をご覧ください。Java HotSpot = VMサーバーコンパイラは、スーパーワードレベルの並列処理を使用した自動ベクトル化をサポートします。これは、ループ内並列処理の単純な場合に限定されます。この記事では、行くJNIルートを正当化します。

3
Paul Jurczak