2011-07-06 5 views
14

일부 자바 원시 컬렉션 (trove, fastutil, hppc)을 살펴 봤는데 클래스 변수가 final 로컬 변수로 선언되는 패턴을 발견했습니다. 예를 들어 :Java의 클래스 변수보다 최종 로컬 변수에 액세스하는 것이 빠릅니까?

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    final int[] key = this.key; 
    final int[] value = this.value; 
    for (int i = 0; i < used.length; i++) { 
     if (used[i]) { 
      p.apply(key[i],value[i]); 
     } 
    } 
} 

좀 벤치마킹을했습니다, 그리고이 일 때 약간 빠른 것 같습니다,하지만 왜이 사건인가? 함수의 처음 세 줄을 주석으로 처리하면 Java가 어떻게 다른지 이해하려고합니다.

참고 : 이것은 this question과 유사하지만 C++ 용이며 final으로 선언 된 이유는 설명하지 않습니다.

+1

당신은 차이를보기 위해 생성 된 자바 어셈블리를 들여다 볼 수 있습니다. –

+0

그냥 그 이유가 HotSpot 컴파일러가 아닌 바이트 코드 자체에있을 수 있다는 것을 깨달았습니다. –

+0

벤치 마크 코드를 게시하십시오. 메소드를 잘못 벤치마킹하여 컴파일러가 아니라 인터프리터 만 테스트했습니다. – Voo

답변

8

final 키워드는 여기에 빨간색 청어입니다. 성능 차이는 두 가지 다른 것을 말하기 때문에 발생합니다.

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    for (int i = 0; i < used.length; i++) { 
    ... 
    } 
} 

은 "부울 배열을 가져, 그리고 해당 배열의 각 요소에 대해 뭔가."말하고 final boolean[] used없이

는,이 함수는 지수가 현재 Object의 used 필드의 현재 값의 길이보다 작을 때, 현재 객체의 used 필드의 현재 값을 가져 와서 함께 뭔가를 할 "말하는 색인 i의 요소. "

used의 값을 변경하게하는 원인을 훨씬 쉽게 판별 할 수 있기 때문에 JIT는 루프 바운드 인바 리언 트가 초과 바인딩 검사를 제거하는 등의 작업을 훨씬 쉽게 할 수 있습니다. p.applyused의 값을 변경할 수있는 경우 여러 스레드를 무시하는 경우에도 JIT는 경계 검사를 제거하거나 다른 유용한 최적화를 수행 할 수 없습니다.

+0

나는 '결말'이 의미하는 바를 빨간 청어라고 혼동합니다. 변수에 액세스하는 것이 더 빠를 필요는 없다는 것을 의미하지만 JIT 컴파일러는 범위 검사 및 조회를 제거하기 위해 루프를 최적화 할 수 있습니까? – job

+0

"다중 스레드 무시"-이 사실을 명확히하기 위해 : JIT ** 만 ** 스레드 로컬 동작을 고려합니다. 이것은 심지어 public을 사용하거나 setter 메소드가 있다고하더라도 다른 스레드에 의해 변경 될 수 있다는 것을 의미합니다. JIT는 이것을 무시할 권리가 있습니다. 따라서 JIT는 실제로 apply()가 참조를 변경하는지 여부를 알아 내야 만합니다 (실제로는 호출을 인라인 할 수 있으면 모든 하위 호출). 통지하지 않으면 그렇지 않다는 것이 가장 확실합니다. – Voo

+0

또한 누군가가 잘못된 자바 벤치 마크를 다시 한 번 써서 (너무 쉽고 올바르게하기가 너무 어려워서) "빠른"행동이 나오는 좋은 기회가 있습니다 - 통역사에 성능 차이가 있어야하지만 적용될 경우 매우 간단합니다. 현대 핫스팟 – Voo

2

해당 메소드 호출의 컨텍스트에서 해당 값이 절대로 변경되지 않으므로 런타임에서 멤버 변수의 값을 지속적으로로드 할 필요가 없음을 런타임에 알려줍니다 (jit). 이것은 약간의 속도 향상을 줄 수 있습니다.

물론 지트가 더 똑똑 해지고 스스로 알아낼 수 있기 때문에 이러한 규칙은별로 유용하지 않게됩니다.

참고, 나는 속도 향상이 마지막 부분보다 로컬 변수를 사용하는 것이 더 낫다는 것을 분명히하지 않았습니다.

+0

이봐 요, 나도 이걸 타이핑 했어! :-) 나는 컴파일러조차도이 참조에서 병렬 변경에 관심이 없다는 것을 알면 도움이 될 수 있다고 생각한다. – Szocske

25

로컬 변수 또는 매개 변수에 액세스하는 것은 한 단계 작업입니다. 스택의 오프셋 N에 위치한 변수를 가져옵니다. (2 개) 인수가가 작동하는 경우 (단순)

  • N = 0 - this
  • N = 1 - 제 인수
  • N = - 2 번째 인수
  • N = 3 - 먼저 로컬 변수
  • N = 4 초의 로컬 변수
  • ...

로컬 변수에 액세스 할 때 고정 오프셋으로 하나의 메모리 액세스 권한이 있습니다 (N은 컴파일 타임에 알려짐).

iload 1 //N = 1 

이 필드를 액세스 할 때, 당신이 실제로 추가 단계를 수행 : 이것은 첫 번째 방법 인수 (int)에 액세스하기위한 바이트 코드입니다. 먼저 현재 개체 주소를 확인하기 위해 "로컬 변수"을 this 읽고 있습니다. 그런 다음 고정 된 오프셋이 this 인 필드 (getfield)를로드하는 중입니다. 따라서 하나가 아닌 두 개의 메모리 연산을 수행합니다 (또는 하나만 추가). 바이트 코드 :

aload 0 //N = 0: this reference 
getfield total I //int total 

기술적으로 로컬 변수 및 매개 변수에 액세스하는 것이 개체 필드보다 빠릅니다. 실제로 많은 다른 요소가 성능에 영향을 미칠 수 있습니다 (다양한 수준의 CPU 캐시 및 JVM 최적화 포함).

final은 다른 이야기입니다. 그것은 기본적으로 컴파일러/JIT에 대한 힌트로,이 참조는 변경되지 않으므로 더 무거운 최적화를 할 수 있습니다. 그러나 엄지 손가락 규칙은 가능한 경우 언제든지 final을 사용하여 추적하는 것이 훨씬 더 어렵습니다.

+5

이 답변 (특히 마지막 단락)이 표시된 것보다 낫다고 생각합니다. –

+0

final의 속도 향상 중 일부는 객체가 범위를 벗어나기 전에 포인터를 재사용 할 수 있다는 것을 스마트 JIT가 알 수 있고 alloc()을 저장하고 약간 더 작은 메모리를 사용하여 더 나은 캐시 히트를 얻는 것이 아닌지 궁금합니다. 발자국 ... – Ajax

+0

전혀 동의합니다. 가장 유용한 대답입니다. – omniyo

1

생성 된 VM opcode에서 로컬 변수는 피연산자 스택의 항목이고 필드 참조는 객체 참조를 통해 값을 검색하는 명령을 통해 스택으로 이동해야합니다. JIT가 스택 참조가 참조를보다 쉽게 ​​등록 할 수 있다고 생각합니다.

+2

맞지 않습니다. 지역 변수는 * operand stack *이 아닌 thread * stack *에 배치됩니다. 다양한'load' /'store' 연산 코드는 로컬 변수를 스택에서 피연산자 스택으로 그리고 뒤로 이동 시키는데 사용됩니다. [이 이미지] (http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/fig01.gif)를 참조하십시오. –

0

이러한 간단한 최적화는 이미 JVM 런타임에 포함되어 있습니다. JVM이 인스턴스 변수에 대한 순진한 접근을한다면 Java 애플리케이션은 거북해질 것입니다.

예를 들어, 더 간단한 JVM의 경우 이러한 수동 조정은 아마도 가치가 있습니다. 기계적 인조 인간.

+0

덱스 (안드로이드) 바이트 코드는 아마도 더 효과적 일 것입니다 ... 압축되지 않은 .dx는 jar 압축 .class보다 작습니다. 자바 모바일에 대한 dalvik의 전체적인 이유는 성능입니다 (표준 jvm은 모바일 장치에서 너무 부 풀립니다) – Ajax

관련 문제