x64 Java에서 int보다 긴 이유는 무엇입니까?


91

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에서 다시 시도했지만 큰 변화는 없습니다.


8
가장 가능성이 높은 것은 CPU 또는 JVM의 다양한 부분이 아닌 설정입니다. 이 측정을 안정적으로 재현 할 수 있습니까? 루프를 반복하지 않고, JIT를 워밍업하지 않고,을 사용하고 currentTimeMillis(), 완전히 최적화 할 수있는 코드를 실행하는 등 신뢰할 수없는 결과가 나옵니다.

1
나는 얼마 전에 벤치마킹하고 있었는데, 나는 long루프 카운터로 를 사용해야했다 . JIT 컴파일러가 int. 생성 된 기계어 코드의 분해를 살펴볼 필요가 있습니다.
Sam

7
이것은 올바른 마이크로 벤치 마크가 아니며 그 결과가 어떤 식 으로든 현실을 반영 할 것이라고 기대하지 않습니다.
Louis Wasserman 2013

7
적절한 자바 마이크로 벤치 마크를 작성하지 못한 것에 대해 OP를 비난하는 모든 코멘트는 말할 수없이 게으르다. 이것은 JVM이 코드에 대해 수행하는 작업을보고 확인하면 매우 쉽게 알아낼 수있는 종류입니다.
tmyklebu 2013

2
@maaartinus : 허용 된 연습은 알려진 함정 목록을 중심으로 작동하기 때문에 허용되는 연습입니다. 적절한 Java 벤치 마크의 경우 스택 대체가 아닌 적절하게 최적화 된 코드를 측정하고 있는지 확인하고 마지막에 측정 값이 깨끗한 지 확인하려고합니다. OP는 완전히 다른 문제를 발견했으며 그가 제공 한 벤치 마크가이를 적절히 입증했습니다. 그리고 앞서 언급했듯이이 코드를 적절한 자바 벤치 마크로 바꾸는 것이 실제로 이상 함을 없애주지는 않습니다. 그리고 어셈블리 코드를 읽는 것은 어렵지 않습니다.
tmyklebu 2011

답변:


81

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


8
좋습니다. 따라서 net-net은 long버전이 더 느리다는 것이 아니라 int버전이 더 빠릅니다. 말이 되네요. JIT가 long표현식을 최적화 하는 데 많은 노력을 기울이지 않았을 것 입니다.
Hot Licks 2013

1
... 내 무지를 용서하십시오. 그러나 "funrolled"는 무엇입니까? 나는 용어를 제대로 구글링하지 못하는 것 같고, 인터넷에서 단어가 무엇을 의미하는지 누군가에게 물어봐야하는 것은 이번이 처음이다.
BrianH 2013

1
@BrianDHall gcc-f"플래그"에 대한 명령 줄 스위치로 사용 unroll-loops하고라고 말 하여 최적화를 켭니다 -funroll-loops. 최적화를 설명하기 위해 "unroll"을 사용합니다.
chrylis -cautiouslyoptimistic- 2013

4
@BRPocock : 자바 컴파일러는 할 수 없지만 JIT는 확실히 할 수 있습니다.
tmyklebu 2013

1
명확하게 말하면, 그것은 그것을 "재미있게"하지 않았습니다. 그것은 그것을 풀고 풀린 루프를로 변환했습니다 i-=16. 물론 16 배 더 빠릅니다.
Aleksandr Dubinsky 2013

22

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.


1
@Katona 반드시 그런 것은 아닙니다. 특히 클라이언트와 서버 핫스팟 JVM은 완전히 다른 구현이며 Ilya는 서버 선택을 나타내지 않았습니다 (클라이언트는 일반적으로 32 비트 기본값 임).
chrylis -cautiouslyoptimistic-

1
@tmyklebu 문제는 벤치 마크가 한 번에 여러 가지를 측정한다는 것입니다. 0이 아닌 터미널 조건을 사용하면 변수 수가 줄어 듭니다.
chrylis -cautiouslyoptimistic-

1
@tmyklebu 요점은 OP가 int 대 long의 증가, 감소 및 비교 속도를 비교하려고 의도했다는 것입니다. 대신 (이 답변이 정확하다고 가정) 비교 만 측정하고 0에 대해서만 측정했습니다. 이는 특별한 경우입니다. 다른 것이 없다면 원래 벤치 마크가 오해의 소지가 있습니다. 실제로는 하나의 특정 사례를 측정하지만 세 가지 일반적인 사례를 측정하는 것처럼 보입니다.
yshavit 2013

1
@tmyklebu 오해하지 마십시오. 질문,이 답변 및 귀하의 답변을 찬성했습니다. 그러나 @chrylis가 측정하려는 차이 측정을 중지하기 위해 벤치 마크를 조정하고 있다는 귀하의 진술에 동의하지 않습니다. 내가 틀렸다면 OP가 나를 고칠 수 있지만 == 0벤치 마크 결과에서 불균형 적으로 큰 부분을 차지하는 것처럼 보이지만 주로 측정하려는 것 같지 않습니다 . OP가 더 일반적인 작업 범위를 측정하려고 할 가능성이 더 높으며이 답변은 벤치 마크가 해당 작업 중 하나에 대해 매우 치우쳐 있음을 지적합니다.
yshavit 2013

2
@tmyklebu 전혀 아닙니다. 나는 근본 원인을 이해하기 위해 모두입니다. 그러나, 하나의 주요 원인은 벤치 마크가 기울어 것을 것을 확인하는 데, 그것은 스큐를 제거하는 벤치 마크를 변경 무효 아니다 뿐만 아니라 발굴하고 더 효율적으로 사용할 수 있음을, 그 스큐 (대한 예에 대한 자세한 이해하기 루프를 더 쉽게 풀 수 있도록하는 바이트 코드). 이것이 내가이 답변 (왜곡을 식별 함)과 귀하의 답변 (왜곡을 더 자세히 파고 있음)을 모두 찬성 한 이유입니다.
yshavit 2013

8

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

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

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


4

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

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

@ 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

JNI는 네이티브 코드가 실행되는 동안 GC가 발생할 수 있으므로 어쨌든 몇 가지 불투명 한 핸들을 사용하는 성 가시고 느린 접근 자 메서드를 통해 개체에 액세스해야합니다. 필드를 재정렬하고 패딩을 추가하는 것은 자유 롭습니다.
tmyklebu 2011
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.