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


90

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

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

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

私はこれを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でこれをもう一度試したところ、大きな変更はありませんでした。


8
最も疑わしいのは、CPUやJVMのさまざまな部分ではなく、セットアップです。この測定を確実に再現できますか?ループを繰り返さない、JITをウォームアップしない、使用するcurrentTimeMillis()、完全に最適化できるコードを実行するなど、信頼できない結果が発生します。

1
しばらく前にベンチマークを行っていましlongた。ループカウンターとしてaを使用する必要がありましたint。生成されたマシンコードの逆アセンブルを確認する必要があります。
サム・

7
これは正しいマイクロベンチマークではなく、その結果が何らかの形で現実を反映しているとは思いません。
Louis Wasserman 2013年

7
適切なJavaマイクロベンチマークの記述に失敗したためにOPを批判するコメントはすべて、言葉では言い表せないほど怠惰です。これは、JVMがコードに対して何を行っているかを見て、見ればわかりやすい種類のことです。
tmyklebu 2013年

2
@maaartinus:既知の落とし穴のリストを回避するため、認められた実践は認められた実践です。適切なJavaベンチマークの場合、スタック上での置き換えではなく、適切に最適化されたコードを測定していることを確認し、最後に測定値がクリーンであることを確認します。OPは完全に異なる問題を発見し、彼が提供したベンチマークはそれを適切に実証しました。そして、前述したように、このコードを適切なJavaベンチマークに変換しても、実際には奇妙さは解消されません。アセンブリコードの読み取りは難しくありません。
tmyklebu 2013年

回答:


80

私の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回アンロールしましたが、intは16倍速くなりましたが、longループを。

完全を期すために、私が実際に試したコードは次のとおりです。

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インストールをいじる必要があることに注意してください。ランダムな共有ライブラリを適切な場所に配置する必要があります。そうしないと失敗します。


8
ネットネットはlongバージョンが遅いということではなく、intバージョンが速いということです。それは理にかなっている。JITがlong式を最適化するために多くの努力が費やされなかったようです。
Hot Licks 2013年

1
...私の無知を許してください、しかし「funrolled」とは何ですか?私はその用語を適切にグーグルで検索することすらできないようであり、それが私がインターネットで単語が何を意味するのかを誰かに尋ねなければならなかったのは初めてです。
BrianH 2013

1
@BrianDHall gcc-f「フラグ」のコマンドラインスイッチとして使用し、と言うことでunroll-loops最適化がオンになります-funroll-loops。最適化を説明するために「アンロール」を使用します。
chrylis -cautiouslyoptimistic- 2013年

4
@BRPocock:Javaコンパイラはできませんが、JITはできます。
tmyklebu 2013年

1
明確にするために、それはそれを "funroll"しませんでした。それはそれをアンロールし、アンロールされたループをに変換しましたi-=16。もちろんこれは16倍高速です。
Aleksandr Dubinsky 2013年

22

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

あなたのコードの場合、逆アセンブリは有益です。intOracle JDK 7によってコンパイルされたバージョンのバイトコードは次のとおりです。

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ビットワードを自由に使用できるため、これを複製するよりもコストがかからない可能性は十分にあります。ただし、ここでは分岐ロジックが長くなっています。JVMは、比較する命令はありませんlong、それは定数をプッシュしなければならないので、ゼロを0Lスタック(9)上に、一般的な行うlong比較(10)した後の値に分岐その計算の。

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

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

私はあなたがお勧め正しいマイクロベンチマークの書き込みで同様の比較を行うにJVMを強制するために、中にJITキックを持つ、ともゼロではありません、最終的な条件でこれをしようとする効果を排除するintことがでないことをlong


1
@カトナ必ずしもそうではありません。特に、クライアントとサーバーのHotSpot JVMは完全に異なる実装であり、イリヤはサーバーの選択を指示しませんでした(通常、クライアントは32ビットのデフォルトです)。
クリリス

1
@tmyklebu問題は、ベンチマークが複数の異なるものを一度に測定していることです。ゼロ以外の終了条件を使用すると、変数の数が減ります。
クリリス

1
@tmyklebuポイントは、OPがintとlongの増分、減分、比較の速度を比較することを意図していたということです。代わりに(この答えが正しいと仮定して)比較のみを測定し、0のみを測定しました。これは特殊なケースです。他に何もない場合、元のベンチマークは誤解を招くようになります。実際には特定の1つのケースを測定しているのに、3つの一般的なケースを測定しているように見えます。
yshavit 2013年

1
@tmyklebu誤解しないでください、私は質問、この答えとあなたの答えに賛成票を投じました。しかし、@ chrylisが測定しようとしている差異の測定を停止するためにベンチマークを調整しているというあなたの声明には同意しません。OPは私が間違っていても私を修正できますが、彼らが/主に測定しようとしているようには見えません== 0。これは、ベンチマーク結果の不釣り合いに大きな部分のようです。OPはより一般的な範囲の操作を測定しようとしているように思われますが、この回答は、ベンチマークがこれらの操作の1つだけに大きく偏っていることを指摘しています。
yshavit 2013年

2
@tmyklebuまったくありません。私は根本的な原因を理解するためにすべてです。しかし、一つの大きな原因は、ベンチマークが偏ったことが、それはスキューを削除するには、ベンチマークを変更することが無効ではないのですが、ことが確認されただけでなく、中に掘ると、それはより効率的に有効にすることができ、例えば(スキューそのことについてもっと理解するためにバイトコード、ループの展開などを容易にすることができます。そのため、私はこの答え(スキューを特定する)とあなたの答え(スキューをより詳しく掘り下げる)の両方を支持しました。
yshavit 2013年

8

Java仮想マシンのデータの基本単位は単語です。適切なワードサイズの選択は、JVMの実装に任されています。JVM実装では、32ビットの最小ワードサイズを選択する必要があります。より高いワードサイズを選択して効率を上げることができます。また、64ビットJVMが64ビットワードのみを選択するという制限はありません。

基盤となるアーキテクチャは、ワードサイズも同じであることを規定していません。JVMは、ワード単位でデータを読み書きします。これは、それがために時間がかかっかもしれない理由でもある長いよりもint型

ここでは、同じトピックの詳細を見つけることができます。


4

キャリパーを使用してベンチマークを書いたところです。

結果は、使用のための〜12倍高速化:元のコードとかなり一致しているint以上longtmyklebuまたは非常によく似たものによって報告されたループの展開が起こっているようです。

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;
    }
}

1

参考までに、このバージョンは大まかな「ウォームアップ」を行います。

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%向上しますが、両者の比率はほぼ同じままです。


@TedHopp-私は私のループリミットを変更しようとしましたが、本質的に変更されませんでした。
Hot Licks 2013年

@ Techrocket9:私intはこのコードで同様の数値(20倍高速)を取得します。
tmyklebu 2013年

1

記録のために:

私が使うなら

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

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


0

私は64ビットのマシンでテストする必要はありませんが、かなり大きな違いは、少しだけ長いバイトコードが機能していることを示しています。

32ビット1.7.0_45でlong / int(4400 vs 4800ms)の非常に近い時間が表示されます。

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

編集: これの背後にある理由は、VMがフィールドを並べ替えない可能性があることですを、JNIに干渉する可能性があるため、最適な配置のためにパディングを追加しない可能性があることです。 (そうではありません)。


VMは確かにされ、リオーダのフィールドと追加パディングさせました。
Hot Licks 2013年

ネイティブコードの実行中にGCが発生する可能性があるため、JNIはこれらの煩わしい低速アクセサーメソッドを介してオブジェクトにアクセスする必要があります。フィールドを並べ替えたり、パディングを追加したりするのは自由です。
tmyklebu 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.