2

データ型をあまり気にせずに (つまり、重複するコードをあまり使わずに) いくつかの画像分析アルゴリズムを実装するために、Java でプリミティブ配列のビジター パターンを設定しています。

以下の例では、2 種類の訪問者を定義しています。

  • visitメソッドのシグネチャが存在するプリミティブ型visit(int, int double)
  • visitメソッドのシグネチャがvisit(int, int Double).

これとは別に、両方の訪問者はまったく同じ操作を行います。私のアイデアは、ボックス化/ボックス化解除のコストを試して測定することでした.

だからここに完全なプログラムがあります

public class VisitorsBenchmark {
    public interface Array2DGenericVisitor<TYPE, RET> {

        void begin(int width, int height);

        RET end();

        void visit(int x, int y, TYPE value);
    }

    public interface Array2DPrimitiveVisitor<RET> {

        void begin(final int width, final int height);

        RET end();

        void visit(final int x, final int y, final double value);
    }

    public static <RET>
        RET
        accept(final int width,
               final int height,
               final double[] data,
               final Array2DGenericVisitor<Double, RET> visitor) {

        final int size = width * height;
        visitor.begin(width, height);
        for (int i = 0, x = 0, y = 0; i < size; i++) {
            visitor.visit(x, y, data[i]);
            x++;
            if (x == width) {
                x = 0;
                y++;
                if (y == height) {
                    y = 0;
                }
            }
        }
        return visitor.end();
    }

    public static <RET> RET accept(final int width,
                                   final int height,
                                   final double[] data,
                                   final Array2DPrimitiveVisitor<RET> visitor) {

        final int size = width * height;
        visitor.begin(width, height);
        for (int i = 0, x = 0, y = 0; i < size; i++) {
            visitor.visit(x, y, data[i]);
            x++;
            if (x == width) {
                x = 0;
                y++;
                if (y == height) {
                    y = 0;
                }
            }
        }
        return visitor.end();
    }

    private static final Array2DGenericVisitor<Double, double[]> generic;

    private static final Array2DPrimitiveVisitor<double[]> primitive;

    static {
        generic = new Array2DGenericVisitor<Double, double[]>() {
            private double[] sum;

            @Override
            public void begin(final int width, final int height) {

                final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT));
                sum = new double[length];
            }

            @Override
            public void visit(final int x, final int y, final Double value) {

                final int r = (int) Math.round(Math.sqrt(x * x + y * y));
                sum[r] += value;
            }

            @Override
            public double[] end() {

                return sum;
            }
        };

        primitive = new Array2DPrimitiveVisitor<double[]>() {
            private double[] sum;

            @Override
            public void begin(final int width, final int height) {

                final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT));
                sum = new double[length];
            }

            @Override
            public void visit(final int x, final int y, final double value) {

                final int r = (int) Math.round(Math.sqrt(x * x + y * y));
                sum[r] += value;
            }

            @Override
            public double[] end() {

                return sum;
            }
        };
    }

    private static final int WIDTH = 300;

    private static final int HEIGHT = 300;

    private static final int NUM_ITERATIONS_PREHEATING = 10000;

    private static final int NUM_ITERATIONS_BENCHMARKING = 10000;

    public static void main(String[] args) {

        final double[] data = new double[WIDTH * HEIGHT];
        for (int i = 0; i < data.length; i++) {
            data[i] = Math.random();
        }

        /*
         * Pre-heating.
         */
        for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) {
            accept(WIDTH, HEIGHT, data, generic);
        }
        for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) {
            accept(WIDTH, HEIGHT, data, primitive);
        }

        /*
         * Benchmarking proper.
         */
        double[] sumPrimitive = null;
        double[] sumGeneric = null;

        double aux = System.nanoTime();
        for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) {
            sumGeneric = accept(WIDTH, HEIGHT, data, generic);
        }
        final double timeGeneric = System.nanoTime() - aux;

        aux = System.nanoTime();
        for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) {
            sumPrimitive = accept(WIDTH, HEIGHT, data, primitive);
        }
        final double timePrimitive = System.nanoTime() - aux;

        System.out.println("prim = " + timePrimitive);
        System.out.println("generic = " + timeGeneric);
        System.out.println("generic / primitive = "
                           + (timeGeneric / timePrimitive));
    }
}

私は JIT が非常に巧妙であることを知っているので、両方の訪問者が同じようにうまく機能したことにあまり驚きませんでした。さらに驚くべきことは、ジェネリックビジターがプリミティブよりもわずかに高速に動作するように見えることです。これは予想外です。ベンチマークが難しい場合があることはわかっているので、何か間違ったことをしたに違いありません。エラーを見つけることができますか?

助けてくれてありがとう!!! セバスチャン

[編集]コードを更新して、予熱段階を考慮しました (JIT コンパイラーがその作業を行えるようにするため)。これは結果を変更せず、一貫して 1 未満 (0.95 - 0.98) です。

4

3 に答える 3

2

ベンチマークが難しい場合があることはわかっているので、何か間違ったことをしたに違いありません。エラーを見つけることができますか?

問題は、ベンチマークで JVM のウォームアップが考慮されていないことだと思います。メインメソッドの本体を取り、別のメソッドに入れます。次に、mainその新しいメソッドをループ内で繰り返し呼び出すメソッドを作成します。最後に、結果を調べて、JIT コンパイルやその他のウォームアップ効果によって歪められた最初のいくつかを破棄します。

于 2012-09-10T12:46:45.903 に答える
2

小さなヒント:

  • Math.random()結果は非決定的であるため、ベンチマークの実行には使用しないでください。のようなスマートが必要ですnew Random(xxx)
  • 常に操作の結果を出力します。1 回の実行でベンチマーク タイプを混在させることは、異なる呼び出しサイトの最適化につながる可能性があるため、悪い習慣です (ただし、あなたのケースではありません)。
  • double aux = System.nanoTime(); -- すべてlongsが doubles に収まるわけではありません - 適切に。
  • ベンチマークを実行する環境とハードウェアの仕様を投稿する
  • -XX:-PrintCompilationコンパイルとガベージ コレクション の印刷を有効にして、'staring test' を印刷します-verbosegc -XX:+PrintGCDetails。GC は、結果を歪めるのに十分なだけ、'間違った' テスト中に作動する可能性があります。


編集:

生成されたアセンブラを確認しましたが、どれも本当の理由ではありません。メソッドは完全にインライン化され、最適化されているため、Double.valueOf() の割り当てはありません。CPU レジスタのみを使用します。ただし、ハードウェア仕様/JVM がなければ、本当の答えはありません。

JVM (1.6.0.26) を見つけました。汎用バージョン ( Double) の方がループ アンロール (!) が優れています。これは、より深い分析 (明らかに EA に必要Double.valueOf()) と、WIDTH/HEIGHT の定数フォールディングの可能性があります。WIDTH/HEIGHT をいくつかの素数に変更すると、結果が異なるはずです。


肝心なのは、JVM がどのように最適化し、生成されたマシン コードをチェックするかを理解していない限り、マイクロベンチマークを使用しないことです。


免責事項: 私は JVM エンジニアではありません

于 2012-09-10T14:30:16.327 に答える
0

これは完全に「野蛮な推測」ですが、バイトをスタックにコピーすることに関係していると思います。プリミティブdoubleを渡すには、スタックに8バイトをコピーする必要があります。Doubleを渡すには、ポインタをコピーするだけです。

于 2012-09-10T14:50:07.090 に答える