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은 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 스택은 단어 로 정의되며 , 그 크기는 구현 세부 사항이지만 최소 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를 방해 할 수 있으므로 최적의 정렬을 위해 패딩을 추가하여 여유 시간에 필드 를 재정렬하지 않을 수 있기 때문입니다. (경우가 아님).