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


95

私のJavaプログラムのボトルネックは、ベクトルドット積の束を計算するためのいくつかのタイトなループであるとしましょう。はい、プロファイリングしました、はい、それがボトルネックです、はい、それは重要です、はい、それはまさにアルゴリズムです。はい、Proguardを実行してバイトコードを最適化しました。

仕事は本質的に、ドット積です。同様に、2つfloat[50]あり、ペアワイズ積の合計を計算する必要があります。SSEやMMXのように、この種の操作をすばやく大量に実行するためのプロセッサ命令セットが存在することは知っています。

はい、おそらくJNIでネイティブコードを書くことでこれらにアクセスできます。JNIの呼び出しにはかなりの費用がかかります。

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

おそらく「ノー」です。尋ねる価値があります。


4
見つけるための最も簡単な方法は、おそらくあなたが見つけることができる最新のJITを取得し、生成されたアセンブリをで出力させること-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilationです。ベクトル化可能なメソッドを「ホット」にするのに十分な回数実行するプログラムが必要です。
Louis Wasserman

1
またはソースを見てください。download.java.net/openjdk/jdk7
ビル


3
実際、このブログによれば、JNIを「正しく」使用すると、かなり高速になる可能性があります。
ziggystar 2012

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

回答:


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

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は配列と同じ速さで直接NIOバッファーにアクセスできます。一方、ループを手動でアンロールしても、この場合、パフォーマンスは測定可能なほど向上しません。


3
OpenJDKまたはOracle HotSpotを使用しましたか?一般的な信念に反して、それらは同じではありません。
ジョナサンS.フィッシャー

@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、混合モード)
サミュエルオーデット2012年

1
そのループには、実行されたループ依存性がある可能性があります。ループを2回以上展開することで、さらに高速化できます。

3
@Oliv GCCはコードをSSEでベクトル化しますが、そのような小さなデータの場合、JNI呼び出しのオーバーヘッドは残念ながら大きすぎます。
Samuel Audet

2
JDK 13を搭載した私のA6-7310では、dot():69 ns / dotc():95 nsが得られます。Javaが勝ちました!
ステファンライヒ

39

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

  • 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(opsあたりのナノ秒)-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
同じ論文から:「JIT逆アセンブラー出力は、最適なSIMD命令とそのスケジューリングの呼び出しに関して、実際にはそれほど効率的ではないことを示唆しています。JVMJITコンパイラー(ホットスポット)のソースコードをざっと見てみると、パックされたSIMD命令コードが存在しない。」SSEレジスタはスカラーモードで使用されています。
Aleksandr Dubinsky、2015年

1
@AleksandrDubinsky一部のケースはカバーされていますが、カバーされていないケースもあります。興味のある具体的なケースはありますか?
Nitsan Wakart 2015年

2
問題をひっくり返して、JVMが算術演算を自動ベクトル化するかどうかを尋ねましょう。例を挙げていただけますか?最近、組み込み関数を使用して引き出して書き直さなければならないループがあります。ただし、自動ベクトル化を期待するのではなく、明示的なベクトル化/組み込み機能(agner.org/optimize/vectorclass.pdfと同様)のサポートを希望します。Aparapiの適切なJavaバックエンドを作成することをお勧めします(ただし、そのプロジェクトのリーダーシップにはいくつかの間違った目標があります)。JVMで作業していますか?
Aleksandr Dubinsky

1
@AleksandrDubinskyメールが役立つとは限りませんが、拡張された回答が役立つことを願っています。また、「組み込み関数を使用した書き換え」は、JVMコードを変更して新しい組み込み関数を追加することを意味することに注意してください。つまり、JavaコードをJNIを介したネイティブ実装への呼び出しに置き換えることを意味していたと思います
Nitsan Wakart

1
ありがとうございました。これが正式な答えになるはずです。古く、ベクトル化を実証していないので、論文への参照を削除する必要があると思います。
Aleksandr Dubinsky、2015年

26

Java 7u40以降のHotSpotバージョンでは、サーバーコンパイラが自動ベクトル化をサポートしています。JDK-6340864によると

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


ターゲット化されたSIMD命令セットはそれほど広くありませんが、一部のケースでは、JDK6にもベクトル化があります。
Nitsan Wakart

3
HotSpotでのコンパイラーのベクトル化サポートは、インテルによる貢献により、最近(2017年6月)大幅に改善されました。AVX2を有効にするバグ修正により、パフォーマンス面ではまだリリースされていないjdk9(b163以降)がjdk8に勝っています。ループは、自動ベクトル化が機能するためのいくつかの制約を満たす必要があります。たとえば、使用:intカウンター、定数カウンターインクリメント、ループ不変変数による1つの終了条件、メソッド呼び出しなしのループ本体(?)、手動ループ展開なし!詳細はで利用可能です:cr.openjdk.java.net/~vlivanov/talks/...
Vedran

ベクトル化されたフューズドマルチプルアド(FMA)のサポートは、現在(2017年6月現在)見栄えがよくありません。これは、ベクトル化またはスカラーFMA(?)のいずれかです。ただし、オラクルはAVX-512を使用したFMAベクトル化を可能にするHotSpotへのIntelの貢献を明らかに受け入れたようです。自動ベクトル化ファンとAVX-512ハードウェアにアクセスできる幸運なファンを喜ばせるために、これは(運が良ければ)次のjdk9 EAビルド(b175以降)の1つに現れるかもしれません。
Vedran 2017年

前のステートメントをサポートするためのリンク(:8181616:RFR(M)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でさらに多くのSSE操作)。あなたはあまり期待するべきではありませんが、注意する必要があります。


1
リンクしている記事のいくつかの重要な洞察を要約すると役立ちます。
Aleksandr Dubinsky 2017年

4

OpenClカーネルを作成してコンピューティングを実行し、java http://www.jocl.org/から実行できます

コードはCPUやGPUで実行でき、OpenCL言語はベクトルタイプもサポートしているため、SSE3 / 4命令などを明示的に利用できます。


4

見ていた計算マイクロカーネルの最適な実施のためのJavaとJNIとの性能比較を。彼らは、Java HotSpot VMサーバーコンパイラがスーパーワードレベルの並列処理を使用した自動ベクトル化をサポートしていることを示しています。これは、ループ並列処理の単純なケースに限定されます。この記事では、データサイズがJNIルートを正当化するのに十分な大きさであるかどうかについてのガイダンスも提供します。


3

netlib-javaについて知る前に、あなたがこの質問を書いたと思います;-)マシン最適化された実装で、必要なネイティブAPIを正確に提供し、メモリの固定化により、ネイティブ境界でコストがかかりません。


1
ええ、ずっと前。これが自動的にベクトル化された命令に変換されると聞いてもっと期待していました。しかし、手動で実行することはそれほど難しくありません。
Sean Owen

-4

VMがこの種の最適化に十分にスマートであるかどうか、私はほとんど信じていません。公平にするために、ほとんどの最適化ははるかに単純です。たとえば、2の累乗の場合、乗算ではなくシフトします。monoプロジェクトは、パフォーマンスを支援するために、ネイティブバッキングを使用して独自のベクターやその他のメソッドを導入しました。


3
現在、これを行うJavaホットスポットコンパイラはありませんが、Javaホットスポットコンパイラが行うことほど難しくはありません。SIMD命令を使用して、一度に複数の配列値をコピーします。さらにパターンマッチングとコード生成コードを記述する必要があります。これは、ループの展開を行った後は非常に簡単です。Sunの人々は怠惰になったと思いますが、Oracleで発生するようです(Vladimir氏、これはコードに大いに役立つはずです!):mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
クリストファーマニング2012年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.