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でこれを再試行しましたが、大きな変更はありません。
私のJVMは、long
sを使用すると、内部ループに対してこの非常に簡単なことを行います。
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 */
int
sを使用すると、カンニングします。最初に、私が理解すると主張していないが、展開されたループのセットアップのように見えるいくつかのねじれがあります:
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インストールをいじる必要があることに注意してください。ランダムな共有ライブラリを正確な場所に配置する必要があります。そうしないと失敗します。
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つのもっともらしいシナリオを次に示します。
long
バージョンでより多くの作業を行い、いくつかの追加の値をプッシュおよびポップします。これらは、実際のハードウェア支援CPUスタックではなく、仮想管理スタックにあります。この場合、ウォームアップ後もパフォーマンスに大きな違いが見られます。正しいマイクロベンチマークを書く をお勧めします。JITを有効にして、ゼロではない最終条件でこれを試して、JVMで同じ比較を強制します。 int
は、long
で行います。
Java仮想マシンはWordです。正しいWordサイズの選択は、JVMの実装時に残ります。JVMの実装では、32ビットの最小Wordサイズを選択する必要があります。 64ビットJVMが64ビットWordのみを選択するという制限はありません。
基礎となるアーキテクチャは、Wordのサイズも同じであるというルールを定めていません。 JVMは、データをWord単位で読み書きします。これが、より長い時間がかかる理由です 長いです よりも int。
ここ 同じトピックに関する詳細を参照できます。
caliper を使用してベンチマークを作成しました。
results は、元のコードと非常に一貫性があります。int
をlong
よりも約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;
}
}
レコードの場合:
私が使用する場合
boolean decrementAndCheckLong() {
lo = lo - 1l;
return lo < -1l;
}
(「l--」を「l = l-1l」に変更)長いパフォーマンスが〜50%向上
記録のために、このバージョンは粗い「ウォームアップ」を行います。
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つの間の比率はほぼ同じままです。
テストする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を並べ替えることができないためです。 (そうではありません)。