web-dev-qa-db-ja.com

x64 Javaでintより長い時間がかかるのはなぜですか?

Surface Pro 2タブレットでJava 7 update 45 x64(32ビットJavaがインストールされていない))でWindows 8.1 x64を実行しています。

以下のコードは、iのタイプがlongの場合は1688ms、intがintの場合は109msかかります。 64ビットJVMを備えた64ビットプラットフォームで、long(64ビットタイプ)がintよりも桁違いに遅いのはなぜですか?

私の唯一の推測は、CPUが32ビットの整数よりも64ビットの整数を追加するのに時間がかかるということですが、それはありそうにないことです。 Haswellはリップルキャリー加算器を使用していないと思います。

これをEclipse Kepler SR1で実行しています。

public class Main {

    private static long i = Integer.MAX_VALUE;

    public static void main(String[] args) {    
        System.out.println("Starting the loop");
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheck()){
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheck() {
        return --i < 0;
    }

}

編集:VS 2013(下記)、同じシステムでコンパイルされた同等のC++コードの結果です。 long:72265ms int:74656ms これらの結果は、デバッグ32ビットモードでした。

64ビットリリースモードの場合: 長い:875ms long long:906ms int:1047ms

これは、私が観察した結果が、CPUの制限ではなく、JVM最適化の奇妙さであることを示唆しています。

#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"

long long i = INT_MAX;

using namespace std;


boolean decrementAndCheck() {
return --i < 0;
}


int _tmain(int argc, _TCHAR* argv[])
{


cout << "Starting the loop" << endl;

unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();

cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;



}

編集:Java 8 RTMでこれを再試行しましたが、大きな変更はありません。

90
Techrocket9

私のJVMは、longsを使用すると、内部ループに対してこの非常に簡単なことを行います。

0x00007fdd859dbb80: test   %eax,0x5f7847a(%rip)  /* fun JVM hack */
0x00007fdd859dbb86: dec    %r11                  /* i-- */
0x00007fdd859dbb89: mov    %r11,0x258(%r10)      /* store i to memory */
0x00007fdd859dbb90: test   %r11,%r11             /* unnecessary test */
0x00007fdd859dbb93: jge    0x00007fdd859dbb80    /* go back to the loop top */

intsを使用すると、カンニングします。最初に、私が理解すると主張していないが、展開されたループのセットアップのように見えるいくつかのねじれがあります:

0x00007f3dc290b5a1: mov    %r11d,%r9d
0x00007f3dc290b5a4: dec    %r9d
0x00007f3dc290b5a7: mov    %r9d,0x258(%r10)
0x00007f3dc290b5ae: test   %r9d,%r9d
0x00007f3dc290b5b1: jl     0x00007f3dc290b662
0x00007f3dc290b5b7: add    $0xfffffffffffffffe,%r11d
0x00007f3dc290b5bb: mov    %r9d,%ecx
0x00007f3dc290b5be: dec    %ecx              
0x00007f3dc290b5c0: mov    %ecx,0x258(%r10)   
0x00007f3dc290b5c7: cmp    %r11d,%ecx
0x00007f3dc290b5ca: jle    0x00007f3dc290b5d1
0x00007f3dc290b5cc: mov    %ecx,%r9d
0x00007f3dc290b5cf: jmp    0x00007f3dc290b5bb
0x00007f3dc290b5d1: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b5d5: mov    %r9d,%r8d
0x00007f3dc290b5d8: neg    %r8d
0x00007f3dc290b5db: sar    $0x1f,%r8d
0x00007f3dc290b5df: shr    $0x1f,%r8d
0x00007f3dc290b5e3: sub    %r9d,%r8d
0x00007f3dc290b5e6: sar    %r8d
0x00007f3dc290b5e9: neg    %r8d
0x00007f3dc290b5ec: and    $0xfffffffffffffffe,%r8d
0x00007f3dc290b5f0: shl    %r8d
0x00007f3dc290b5f3: mov    %r8d,%r11d
0x00007f3dc290b5f6: neg    %r11d
0x00007f3dc290b5f9: sar    $0x1f,%r11d
0x00007f3dc290b5fd: shr    $0x1e,%r11d
0x00007f3dc290b601: sub    %r8d,%r11d
0x00007f3dc290b604: sar    $0x2,%r11d
0x00007f3dc290b608: neg    %r11d
0x00007f3dc290b60b: and    $0xfffffffffffffffe,%r11d
0x00007f3dc290b60f: shl    $0x2,%r11d
0x00007f3dc290b613: mov    %r11d,%r9d
0x00007f3dc290b616: neg    %r9d
0x00007f3dc290b619: sar    $0x1f,%r9d
0x00007f3dc290b61d: shr    $0x1d,%r9d
0x00007f3dc290b621: sub    %r11d,%r9d
0x00007f3dc290b624: sar    $0x3,%r9d
0x00007f3dc290b628: neg    %r9d
0x00007f3dc290b62b: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b62f: shl    $0x3,%r9d
0x00007f3dc290b633: mov    %ecx,%r11d
0x00007f3dc290b636: sub    %r9d,%r11d
0x00007f3dc290b639: cmp    %r11d,%ecx
0x00007f3dc290b63c: jle    0x00007f3dc290b64f
0x00007f3dc290b63e: xchg   %ax,%ax /* OK, fine; I know what a nop looks like */

その後、展開されたループ自体:

0x00007f3dc290b640: add    $0xfffffffffffffff0,%ecx
0x00007f3dc290b643: mov    %ecx,0x258(%r10)
0x00007f3dc290b64a: cmp    %r11d,%ecx
0x00007f3dc290b64d: jg     0x00007f3dc290b640

次に、展開されたループの分解コード、それ自体がテストおよび直線ループ:

0x00007f3dc290b64f: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b652: jle    0x00007f3dc290b662
0x00007f3dc290b654: dec    %ecx
0x00007f3dc290b656: mov    %ecx,0x258(%r10)
0x00007f3dc290b65d: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b660: jg     0x00007f3dc290b654

JITはintループを16回展開しましたが、longループはまったく展開しなかったため、intの場合は16倍速くなります。

完全を期すために、実際に試したコードを次に示します。

public class foo136 {
  private static int i = Integer.MAX_VALUE;
  public static void main(String[] args) {
    System.out.println("Starting the loop");
    for (int foo = 0; foo < 100; foo++)
      doit();
  }

  static void doit() {
    i = Integer.MAX_VALUE;
    long startTime = System.currentTimeMillis();
    while(!decrementAndCheck()){
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
  }

  private static boolean decrementAndCheck() {
    return --i < 0;
  }
}

アセンブリダンプは、オプション-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblyを使用して生成されました。この機能を使用するには、JVMインストールをいじる必要があることに注意してください。ランダムな共有ライブラリを正確な場所に配置する必要があります。そうしないと失敗します。

79
tmyklebu

JVMスタックはwordsで定義され、そのサイズは実装の詳細ですが、少なくとも32ビット幅でなければなりません。 JVM実装者mayは64ビットワードを使用しますが、バイトコードはこれに依存できないため、longまたはdouble値を使用した操作は、特に注意して処理する必要があります。 。特に、 JVM整数分岐命令 はタイプintで正確に定義されています。

コードの場合、逆アセンブリは有益です。 Oracle JDK 7でコンパイルされたintバージョンのバイトコードは次のとおりです。

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:I
     3: iconst_1      
     4: isub          
     5: dup           
     6: putstatic     #14  // Field i:I
     9: ifge          16
    12: iconst_1      
    13: goto          17
    16: iconst_0      
    17: ireturn       

JVMは静的i(0)の値をロードし、1を減算(3-4)し、スタックに値を複製し(5)、変数にプッシュバックします(6)ことに注意してください。次に、ゼロと比較するブランチを実行して戻ります。

longのバージョンはもう少し複雑です:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:J
     3: lconst_1      
     4: lsub          
     5: dup2          
     6: putstatic     #14  // Field i:J
     9: lconst_0      
    10: lcmp          
    11: ifge          18
    14: iconst_1      
    15: goto          19
    18: iconst_0      
    19: ireturn       

まず、JVMがスタック(5)で新しい値を複製するとき、2つのスタックワードを複製する必要があります。あなたの場合、JVMは便利であれば64ビットのWordを自由に使用できるので、これは1つを複製するよりも高価ではない可能性が十分にあります。ただし、ブランチロジックがここにあることに気付くでしょう。 JVMにはlongをゼロと比較する命令がないため、定数0Lをスタックにプッシュする必要があり(9)、一般的なlong比較(10)を行います、そしてthat計算の値で分岐します。

2つのもっともらしいシナリオを次に示します。

  • JVMはバイトコードパスを正確にたどっています。この場合、longバージョンでより多くの作業を行い、いくつかの追加の値をプッシュおよびポップします。これらは、実際のハードウェア支援CPUスタックではなく、仮想管理スタックにあります。この場合、ウォームアップ後もパフォーマンスに大きな違いが見られます。
  • JVMは、このコードを最適化できることを認識しています。この場合、実際に不要なプッシュ/比較ロジックの一部を最適化するのに余分な時間がかかります。この場合、ウォームアップ後のパフォーマンスの違いはほとんどありません。

正しいマイクロベンチマークを書く をお勧めします。JITを有効にして、ゼロではない最終条件でこれを試して、JVMで同じ比較を強制します。 intは、longで行います。

22
chrylis

Java仮想マシンはWordです。正しいWordサイズの選択は、JVMの実装時に残ります。JVMの実装では、32ビットの最小Wordサイズを選択する必要があります。 64ビットJVMが64ビットWordのみを選択するという制限はありません。

基礎となるアーキテクチャは、Wordのサイズも同じであるというルールを定めていません。 JVMは、データをWord単位で読み書きします。これが、より長い時間がかかる理由です 長いです よりも int

ここ 同じトピックに関する詳細を参照できます。

8
Vaibhav Raj

caliper を使用してベンチマークを作成しました。

results は、元のコードと非常に一貫性があります。intlongよりも約12倍高速化します。確かに、ループの展開 tmyklebuによる報告 または非常に類似した何かが起こっているようです。

timeIntDecrements         195,266,845.000
timeLongDecrements      2,321,447,978.000

これは私のコードです。 caliperの新しく作成されたスナップショットを使用していることに注意してください。既存のベータリリースに対してコーディングする方法がわからなかったためです。

package test;

import com.google.caliper.Benchmark;
import com.google.caliper.Param;

public final class App {

    @Param({""+1}) int number;

    private static class IntTest {
        public static int v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    private static class LongTest {
        public static long v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    @Benchmark
    int timeLongDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            LongTest.reset();
            while (!LongTest.decrementAndCheck()) { k++; }
        }
        return (int)LongTest.v | k;
    }    

    @Benchmark
    int timeIntDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            IntTest.reset();
            while (!IntTest.decrementAndCheck()) { k++; }
        }
        return IntTest.v | k;
    }
}
4
tucuxi

レコードの場合:

私が使用する場合

boolean decrementAndCheckLong() {
    lo = lo - 1l;
    return lo < -1l;
}

(「l--」を「l = l-1l」に変更)長いパフォーマンスが〜50%向上

1
R.Moeller

記録のために、このバージョンは粗い「ウォームアップ」を行います。

public class LongSpeed {

    private static long i = Integer.MAX_VALUE;
    private static int j = Integer.MAX_VALUE;

    public static void main(String[] args) {

        for (int x = 0; x < 10; x++) {
            runLong();
            runWord();
        }
    }

    private static void runLong() {
        System.out.println("Starting the long loop");
        i = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckI()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the long loop in " + (endTime - startTime) + "ms");
    }

    private static void runWord() {
        System.out.println("Starting the Word loop");
        j = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckJ()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the Word loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheckI() {
        return --i < 0;
    }

    private static boolean decrementAndCheckJ() {
        return --j < 0;
    }

}

全体の時間は約30%向上しますが、2つの間の比率はほぼ同じままです。

1
Hot Licks

テストする64ビットマシンはありませんが、かなり大きな違いは、動作中のバイトコードがわずかに長いだけではないことを示しています。

32ビット1.7.0_45でlong/intに非常に近い時間(4400対4800ms)が見られます。

これはguessだけですが、I stronglyは、メモリの不整列のペナルティの影響だと疑っています。疑いを確認/拒否するには、public static int dummy = 0;を追加してみてください。 before iの宣言。これにより、メモリレイアウトでiが4バイト押し下げられ、パフォーマンスが向上するように適切に調整される場合があります。 問題の原因ではないことを確認しました。

編集: この背後にある理由は、VMは、JNIに干渉する可能性があるため、最適な位置合わせのために余暇にパディングを追加してfieldsを並べ替えることができないためです。 (そうではありません)。

0
Durandal