101

Let's say the bottleneck of my Java program really is some tight loops to compute a bunch of vector dot products. Yes I've profiled, yes it's the bottleneck, yes it's significant, yes that's just how the algorithm is, yes I've run Proguard to optimize the byte code, etc.

The work is, essentially, dot products. As in, I have two float[50] and I need to compute the sum of pairwise products. I know processor instruction sets exist to perform these kind of operations quickly and in bulk, like SSE or MMX.

Yes I can probably access these by writing some native code in JNI. The JNI call turns out to be pretty expensive.

I know you can't guarantee what a JIT will compile or not compile. Has anyone ever heard of a JIT generating code that uses these instructions? and if so, is there anything about the Java code that helps make it compilable this way?

Probably a "no"; worth asking.

4

9 に答える 9

46

したがって、基本的には、コードをより高速に実行する必要があります。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 バッファーにアクセスできます。一方、この場合、ループを手動で展開しても、パフォーマンスが大幅に向上することはありません。

于 2012-05-30T02:06:53.873 に答える
42

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

  • 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):

ホット ループのアセンブリをフォーマットしてここに貼り付けるには少し手間がかかりますが、ここにスニペットを示します (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

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

于 2013-06-17T08:00:33.707 に答える
26

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

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

于 2013-11-28T13:30:13.503 に答える
4

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

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

于 2013-02-28T15:02:34.523 に答える
3

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

于 2015-12-01T16:57:21.600 に答える
-6

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

于 2012-05-30T02:40:42.500 に答える