x64 Java에서 int보다 긴 이유는 무엇입니까? System.out.println(“Finished

Surface Pro 2 태블릿에서 Java 7 업데이트 45 x64 (32 비트 Java가 설치되지 않음)와 함께 Windows 8.1 x64를 실행하고 있습니다.

아래 코드는 i 유형이 길면 1688ms, i가 정수이면 109ms가 걸립니다. 64 비트 JVM이있는 64 비트 플랫폼에서 long (64 비트 유형)이 int보다 훨씬 느린 이유는 무엇입니까?

내 유일한 추측은 CPU가 32 비트 정수보다 64 비트 정수를 추가하는 데 더 오래 걸리지 만 그럴 것 같지 않다는 것입니다. Haswell이 잔물결 운반 가산기를 사용하지 않는 것 같습니다.

Eclipse Kepler SR1, btw에서 이것을 실행하고 있습니다.

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은 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 설치를 엉망으로 만들어야합니다. 임의의 공유 라이브러리를 정확한 위치에 배치해야합니다. 그렇지 않으면 실패합니다.


답변

JVM 스택은 단어 로 정의되며 , 그 크기는 구현 세부 사항이지만 최소 32 비트 너비 여야합니다. JVM 구현 자는 64 비트 단어를 사용할 있지만 바이트 코드는 이에 의존 할 수 없으므로 long또는 double값을 사용하는 작업 은 특별히주의하여 처리해야합니다. 특히 JVM 정수 분기 명령 은 정확히 유형에 정의되어 int있습니다.

코드의 경우 분해가 도움이됩니다. 다음 int은 Oracle 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)로 다시 푸시합니다. 그런 다음 0과 비교 분기를 수행하고 반환합니다.

가있는 버전 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)에 새 값을 복제 할 때 두 개의 스택 단어를 복제해야합니다. 귀하의 경우 JVM은 편리하다면 64 비트 단어를 자유롭게 사용할 수 있기 때문에 복제하는 것보다 더 비싸지 않을 수 있습니다. 그러나 여기서 분기 논리가 더 길다는 것을 알 수 있습니다. JVM에는 a long를 0과 비교하는 명령이 없으므로 상수 0L를 스택 (9) 으로 푸시 하고 일반 long비교 (10)를 수행 한 다음 해당 계산 값을 분기 해야 합니다.

다음은 그럴듯한 두 가지 시나리오입니다.

  • JVM은 정확히 바이트 코드 경로를 따릅니다. 이 경우 long버전 에서 더 많은 작업을 수행하고 몇 가지 추가 값을 푸시하고 팝 하며 이는 실제 하드웨어 지원 CPU 스택이 아닌 가상 관리 스택 에 있습니다. 이 경우 워밍업 후에도 상당한 성능 차이를 볼 수 있습니다.
  • JVM은이 코드를 최적화 할 수 있음을 인식합니다. 이 경우 실제로 불필요한 푸시 / 비교 로직 중 일부를 최적화하는 데 추가 시간이 걸립니다. 이 경우 예열 후 성능 차이가 거의 나타나지 않습니다.

난 당신이 추천 정확한 마이크로 벤치 쓰기 의 JIT 킥을 가지고, 또한에서 동일한 비교를 수행 할 JVM을 강제로 0이 아닌 최종 조건이 노력의 효과를 제거하기 int가 함께한다는 것을를 long.


답변

Java Virtual Machine에서 데이터의 기본 단위는 단어입니다. 올바른 단어 크기를 선택하는 것은 JVM 구현에 달려 있습니다. JVM 구현은 32 비트의 최소 단어 크기를 선택해야합니다. 효율성을 얻기 위해 더 높은 단어 크기를 선택할 수 있습니다. 64 비트 JVM이 64 비트 워드 만 선택해야한다는 제한도 없습니다.

기본 아키텍처는 단어 크기도 동일해야한다고 규정하지 않습니다. JVM은 단어 단위로 데이터를 읽고 / 씁니다. 이것이 int 보다 오래 걸리는 이유 입니다.

여기 에서 동일한 주제에 대해 더 많이 찾을 수 있습니다.


답변

방금 caliper를 사용하여 벤치 마크를 작성했습니다 .

결과를 사용하기위한 ~ 12 배 속도 향상 : 원래의 코드와 상당히 일치 int이상 long. 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;
    }
}


답변

기록을 위해이 버전은 조잡한 “예열”을 수행합니다.

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 % 개선되지만 둘 사이의 비율은 거의 동일하게 유지됩니다.


답변

기록을 위해 :

내가 사용한다면

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

( “l–“을 “l = l-1l”로 변경) 긴 성능이 ~ 50 % 향상됩니다.


답변

테스트 할 64 비트 머신은 없지만 다소 큰 차이는 작업에서 약간 더 긴 바이트 코드 이상이 있음을 나타냅니다.

32 비트 1.7.0_45에서 long / int (4400 vs 4800ms)에 대한 매우 가까운 시간이 보입니다.

이것은 추측 일 뿐이지 만 메모리 정렬 불량 페널티의 효과 라고 강력히 의심합니다. 의심을 확인 / 거부하려면 public static int dummy = 0을 추가해보십시오. i 선언 전에 . 그러면 메모리 레이아웃에서 i를 4 바이트까지 밀어 내고 더 나은 성능을 위해 올바르게 정렬 할 수 있습니다. 문제를 일으키지 않는 것으로 확인되었습니다.

편집하다: 그 이유는 VM이 JNI를 방해 할 수 있으므로 최적의 정렬을 위해 패딩을 추가하여 여유 시간에 필드 를 재정렬하지 않을 수 있기 때문입니다. (경우가 아님).