4

java for loops を使用していくつかの実行時テストを行い、奇妙な動作を認識しました。私のコードでは、io と出力パラメーターをシミュレートするために、int、double などのプリミティブ型のラッパー オブジェクトが必要ですが、それは重要ではありません。私のコードを見てください。フィールド アクセスを持つオブジェクトは、プリミティブ型よりもどのように高速化できますか?

forプリミティブ型のループ:

public static void main(String[] args) {
    double max = 1000;
    for (int j = 1; j < 8; j++) {
        double i;
        max = max * 10;
        long start = System.nanoTime();
        for (i = 0; i < max; i++) {
        }
        long end = System.nanoTime();
        long microseconds = (end - start) / 1000;
        System.out.println("MicroTime primitive(max: ="+max + "): " + microseconds);
    }
}

結果:

MicroTime プリミティブ (最大: =10000.0): 110
MicroTime プリミティブ (最大: =100000.0): 1081
MicroTime プリミティブ (最大: =1000000.0): 2450
MicroTime プリミティブ (最大: =1.0E7): 28248
MicroTime プリミティブ (最大: =1.0E8) : 276205
MicroTime プリミティブ (最大: =1.0E9): 2729824
MicroTime プリミティブ (最大: =1.0E10): 27547009

for単純型のループ (ラッパー オブジェクト):

public static void main(String[] args) {
    HDouble max = new HDouble();
    max.value = 1000;
    for (int j = 1; j < 8; j++) {
        HDouble i = new HDouble();
        max.value = max.value*10;
        long start = System.nanoTime();
        for (i.value = 0; i.value <max.value; i.value++) {
        }
        long end = System.nanoTime();
        long microseconds = (end - start) / 1000;
        System.out.println("MicroTime wrapper(max: ="+max.value + "): " + microseconds);
    }
}

結果:

MicroTime ラッパー (最大: =10000.0): 157
MicroTime ラッパー (最大: =100000.0): 1561
MicroTime ラッパー (最大: =1000000.0): 3174
MicroTime ラッパー (最大: =1.0E7): 15630
MicroTime ラッパー (最大: =1.0E8) : 155471
MicroTime ラッパー (最大: =1.0E9): 1520967
MicroTime ラッパー (最大: =1.0E10): 15373311

反復回数が多いほど、2 番目のコードは高速になります。しかし、なぜ?java-compiler と jvm がコードを最適化していることは知っていますが、プリミティブ型がフィールド アクセスを持つオブジェクトよりも遅くなる可能性があるとは思いもしませんでした。
誰かがそれについてもっともらしい説明を持っていますか?

編集: HDouble クラス:

public class HDouble {
    public double value;

    public HDouble() {
    }

    public HDouble(double value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

コードを含むループもテストしました。たとえば、合計を計算します -> 同じ動作です (違いはそれほど大きくありませんが、原始的なアルゴリズムはもっと速くなければならないと思いましたか?)。最初に私が考えたのは、計算にそれだけの時間がかかり、フィールド アクセスにほとんど違いがないことです。

ラッパー for ループ:

for (i.value = 0; i.value <max.value; i.value++) {
    sum.value = sum.value + i.value;
}

結果:

MicroTime ラッパー (最大: =10000.0): 243
MicroTime ラッパー (最大: =100000.0): 2805
MicroTime ラッパー (最大: =1000000.0): 3409
MicroTime ラッパー (最大: =1.0E7): 28104
MicroTime ラッパー (最大: =1.0E8) : 278432
MicroTime ラッパー (最大: =1.0E9): 2678322
MicroTime ラッパー (最大: =1.0E10): 26665540

プリミティブ for ループ:

for (i = 0; i < max; i++) {
    sum = sum + i;
}

結果:

MicroTime プリミティブ (最大: =10000.0): 149
MicroTime プリミティブ (最大: =100000.0): 1996
MicroTime プリミティブ (最大: =1000000.0): 2289
MicroTime プリミティブ (最大: =1.0E7): 27085
MicroTime プリミティブ (最大: =1.0E8) : 279939
MicroTime プリミティブ (最大: =1.0E9): 2759133
MicroTime プリミティブ (最大: =1.0E10): 27369724

4

1 に答える 1

11

手作りのマイクロベンチマークにだまされるのはとても簡単です。実際に何を測定しているのかわかりません。そのため、 JMHのような特別なツールがあります。しかし、原始的な手作りのベンチマークに何が起こるかを分析してみましょう。

static class HDouble {
    double value;
}

public static void main(String[] args) {
    primitive();
    wrapper();
}

public static void primitive() {
    long start = System.nanoTime();
    for (double d = 0; d < 1000000000; d++) {
    }
    long end = System.nanoTime();
    System.out.printf("Primitive: %.3f s\n", (end - start) / 1e9);
}

public static void wrapper() {
    HDouble d = new HDouble();
    long start = System.nanoTime();
    for (d.value = 0; d.value < 1000000000; d.value++) {
    }
    long end = System.nanoTime();
    System.out.printf("Wrapper:   %.3f s\n", (end - start) / 1e9);
}

結果はあなたのものと多少似ています:

Primitive: 3.618 s
Wrapper:   1.380 s

テストを数回繰り返します。

public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
        primitive();
        wrapper();
    }
}

もっと面白くなります:

Primitive: 3.661 s
Wrapper:   1.382 s
Primitive: 3.461 s
Wrapper:   1.380 s
Primitive: 1.376 s <-- starting from 3rd iteration
Wrapper:   1.381 s <-- the timings become equal
Primitive: 1.371 s
Wrapper:   1.372 s
Primitive: 1.379 s
Wrapper:   1.378 s

両方の方法が最終的に最適化されたようです。もう一度実行して、JIT コンパイラ アクティビティをログに記録します。 -XX:-TieredCompilation -XX:CompileOnly=Test -XX:+PrintCompilation

    136    1 %           Test::primitive @ 6 (53 bytes)
   3725    1 %           Test::primitive @ -2 (53 bytes)   made not entrant
Primitive: 3.589 s
   3748    2 %           Test::wrapper @ 17 (73 bytes)
   5122    2 %           Test::wrapper @ -2 (73 bytes)   made not entrant
Wrapper:   1.374 s
   5122    3             Test::primitive (53 bytes)
   5124    4 %           Test::primitive @ 6 (53 bytes)
Primitive: 3.421 s
   8544    5             Test::wrapper (73 bytes)
   8547    6 %           Test::wrapper @ 17 (73 bytes)
Wrapper:   1.378 s
Primitive: 1.372 s
Wrapper:   1.375 s
Primitive: 1.378 s
Wrapper:   1.373 s
Primitive: 1.375 s
Wrapper:   1.378 s

%最初の反復でコンパイル ログにサインインすることに注意してください。これは、メソッドが OSR (オンスタック置換)モードでコンパイルされたことを意味します。2 回目の反復中に、メソッドは通常モードで再コンパイルされました。その後、3 回目以降はプリミティブとラッパーの実行速度に差はありませんでした。

実際に測定したのは、OSR スタブのパフォーマンスです。通常、これはアプリケーションの実際のパフォーマンスには関係がなく、あまり気にする必要はありません。

しかし、まだ問題が残っています。なぜラッパーの OSR スタブは、プリミティブ変数よりも適切にコンパイルされるのでしょうか? これを確認するには、生成されたアセンブリ コードに取り掛かる必要があります。
-XX:CompileOnly=Test -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

コンパイルされたループだけを残して、関係のないコードはすべて省略します。

原生的:

0x00000000023e90d0: vmovsd 0x28(%rsp),%xmm1      <-- load double from the stack
0x00000000023e90d6: vaddsd -0x7e(%rip),%xmm1,%xmm1
0x00000000023e90de: test   %eax,-0x21f90e4(%rip)
0x00000000023e90e4: vmovsd %xmm1,0x28(%rsp)      <-- store to the stack
0x00000000023e90ea: vucomisd 0x28(%rsp),%xmm0    <-- compare with the stack value
0x00000000023e90f0: ja     0x00000000023e90d0

ラッパー:

0x00000000023ebe90: vaddsd -0x78(%rip),%xmm0,%xmm0
0x00000000023ebe98: vmovsd %xmm0,0x10(%rbx)      <-- store to the object field
0x00000000023ebe9d: test   %eax,-0x21fbea3(%rip)
0x00000000023ebea3: vucomisd %xmm0,%xmm1         <-- compare registers
0x00000000023ebea7: ja     0x00000000023ebe90

ご覧のとおり、「プリミティブ」の場合は、スタックの場所に対して多数のロードとストアが行われますが、「ラッパー」の場合は主にレジスタ内操作が行われます。OSR スタブがスタックを参照する理由は非常に理解できます。インタープリター モードではローカル変数がスタックに格納され、OSR スタブはこのインタープリタード フレームと互換性があります。「ラッパー」の場合、値はヒープに格納され、オブジェクトへの参照はすでにレジスタにキャッシュされています。

于 2015-11-22T19:07:49.487 に答える