Java에서 최종 키워드를 사용하면 성능이 향상됩니까?
자바에서는 final
키워드를 사용할 수있는 곳이 많지만 그 용도는 드물다.
예를 들면 다음과 같습니다.
String str = "abc";
System.out.println(str);
위의 경우에는 str
가능 final
하지만 일반적으로 해제되어 있습니다.
메서드를 재정의하지 않을 때는 final 키워드를 사용할 수 있습니다. 상속되지 않는 클래스의 경우와 유사합니다.
이러한 경우 중 하나 또는 모두에 최종 키워드를 사용하면 실제로 실적이 향상됩니까? 그렇다면 어떻게? 설명 해주십시오. 제대로 사용하는 final
것이 실제로 성능에 중요한 경우, Java 프로그래머는 키워드를 최대한 활용하기 위해 어떤 습관을 개발해야합니까?
보통은 아닙니다. 가상 메소드의 경우, HotSpot은 메소드가 실제로 대체 되었는지 여부를 추적하고 메소드가 대체 되지 않은 것으로 가정 하여 인라인과 같은 최적화를 수행 할 수 있습니다. 이때 메소드를 대체하는 클래스를로드 할 때까지 이러한 최적화를 취소 (또는 부분적으로 취소) 할 수 있습니다.
(물론 이것은 HotSpot을 사용한다고 가정하지만 가장 일반적인 JVM이므로 ...)
내 생각 final
에는 성능상의 이유보다는 명확한 디자인과 가독성을 기반으로 사용해야합니다 . 성능상의 이유로 변경하려는 경우 가장 명확한 코드를 구부리기 전에 적절한 측정을 수행해야합니다. 이렇게하면 추가 성능이 가독성 / 디자인의 가치가 없는지 결정할 수 있습니다. (내 경험상 YMMV와 같은 가치는 거의 없습니다.)
편집 : 최종 필드가 언급되었으므로 명확한 디자인 측면에서 종종 좋은 아이디어라는 사실을 제기하는 것이 좋습니다. 또한 스레드 간 가시성 측면에서 보장 된 동작을 변경합니다. 생성자가 완료된 후 최종 필드는 다른 스레드에서 즉시 볼 수 있습니다. 이것은 아마도 가장 일반적인 사용하는 것입니다 final
하지만 조쉬 블로흐의의 후원자로, 내 경험에서 "상속 설계하거나 금지"엄지 손가락의 규칙은, 아마 사용해야하는 final
클래스를 더 자주 ...
짧은 대답 : 걱정하지 마십시오!
긴 대답 :
최종 지역 변수 에 대해 이야기 할 때 키워드를 사용 final
하면 컴파일러가 코드를 정적으로 최적화하는 데 도움 이되고 결과적으로 코드가 더 빨라질 수 있습니다. 예를 a + b
들어 아래 예제 의 최종 문자열 은 정적으로 (컴파일시) 연결됩니다.
public class FinalTest {
public static final int N_ITERATIONS = 1000000;
public static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
public static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
long tStart, tElapsed;
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method with finals took " + tElapsed + " ms");
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testNonFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method without finals took " + tElapsed + " ms");
}
}
결과?
Method with finals took 5 ms
Method without finals took 273 ms
Java Hotspot VM 1.7.0_45-b18에서 테스트되었습니다.
실제 성능이 얼마나 향상 되었습니까? 감히 말하지 않습니다. 대부분의 경우, 아마도 문자열 연결을 피할 수 있기 때문에이 합성 테스트에서 ~ 270 나노초 (아마도 드물지만)이지만 고도로 최적화 된 유틸리티 코드에서는 한 가지 요인 이 될 수 있습니다. 어쨌든 원래 질문에 대한 대답은 그렇습니다. 성능을 향상시킬 수는 있지만 조금 이라도 좋습니다 .
컴파일 타임의 이점을 제외하고는 키워드를 사용하면 final
성능에 상당한 영향을 미친다는 증거를 찾을 수 없었 습니다.
예, 그럴 수 있습니다. final이 성능을 향상시킬 수있는 인스턴스는 다음과 같습니다.
조건부 컴파일 은 특정 조건에 따라 코드 줄을 클래스 파일로 컴파일하지 않는 기술입니다. 프로덕션 빌드에서 수많은 디버깅 코드를 제거하는 데 사용할 수 있습니다.
다음을 고려하세요:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (doSomething) {
// do first part.
}
if (doSomething) {
// do second part.
}
if (doSomething) {
// do third part.
}
if (doSomething) {
// do finalization part.
}
}
doSomething 속성을 최종 속성으로 변환하여 컴파일러에게 doSomething이 표시 될 때마다 컴파일 타임 대체 규칙에 따라 false로 대체해야한다고 컴파일러에 알 렸습니다. 컴파일러의 첫 번째 패스는 코드를 다음 과 같이 변경합니다 .
public class ConditionalCompile {
private final static boolean doSomething= false;
if (false){
// do first part.
}
if (false){
// do second part.
}
if (false){
// do third part.
}
if (false){
// do finalization part.
}
}
이 작업이 완료되면 컴파일러는 다시 살펴보고 코드에 도달 할 수없는 명령문이 있는지 확인합니다. 최고 품질의 컴파일러로 작업하고 있기 때문에 도달 할 수없는 모든 바이트 코드를 좋아하지는 않습니다. 그래서 그것들을 제거하고 결국 이것으로 끝납니다.
public class ConditionalCompile {
private final static boolean doSomething= false;
public static void someMethodBetter( ) {
// do first part.
// do second part.
// do third part.
// do finalization part.
}
}
따라서 과도한 코드 또는 불필요한 조건부 검사가 줄어 듭니다.
편집 : 예를 들어 다음 코드를 보자.
public class Test {
public static final void main(String[] args) {
boolean x = false;
if (x) {
System.out.println("x");
}
final boolean y = false;
if (y) {
System.out.println("y");
}
if (false) {
System.out.println("z");
}
}
}
Java 8로이 코드를 컴파일하고 디 컴파일 javap -c Test.class
하면 다음 과 같은 결과 를 얻습니다.
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static final void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: ifeq 14
6: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #22 // String x
11: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: iconst_0
15: istore_2
16: return
}
컴파일 된 코드에는 비 최종 변수 만 포함되어 x
있습니다. 이로 인해 최종 변수가 최소한이 간단한 경우 성능에 영향을 미치게됩니다.
IBM에 따르면-클래스 또는 메소드는 아닙니다.
http://www.ibm.com/developerworks/java/library/j-jtp04223.html
실제로 두 가지 (적어도) 다른 경우에 대해 묻습니다.
final
지역 변수final
메소드 / 클래스
Jon Skeet은 이미 2)로 답변했습니다. 약 1) :
나는 그것이 차이를 만들지 않는다고 생각합니다. 로컬 변수의 경우 컴파일러는 변수가 최종 변수인지 여부를 추론 할 수 있습니다 (단순히 두 번 이상 지정되었는지 확인하여). 따라서 컴파일러가 한 번만 할당 된 변수를 최적화하려는 경우 변수가 실제로 선언되었는지 여부에 관계없이 그렇게 할 수 있습니다 final
.
final
보호 / 공공 클래스 필드에 차이를 만들 수 있습니다. 컴파일러가 필드가 두 번 이상 설정되어 있는지 확인하는 것은 매우 어렵습니다. 다른 클래스 (로드되지 않았을 수도 있음)에서 발생할 수 있기 때문입니다. 그러나 JVM조차도 Jon이 설명하는 기술을 사용할 수 있습니다 (낙관적으로 최적화하고 클래스가로드되면 필드를 변경하면 되돌립니다).
요약하면 성능에 도움이되는 이유는 없습니다. 따라서 이런 종류의 미세 최적화는 도움이되지 않습니다. 확인하기 위해 벤치마킹을 시도해 볼 수는 있지만 차이가 있을지 의심됩니다.
편집하다:
실제로 Timo Westkämper의 답변에 따르면 경우에 따라 수업 분야의 성능을 향상시킬 final
수 있습니다 . 나는 정정되었다.
아무도 실제로 약간의 차이가 있음을 증명하기 위해 디 컴파일 된 실제 코드를 게시하지 않은 것에 놀랐습니다.
참고로이에 대해 테스트 된 javac
버전 8
, 9
및 10
.
이 방법을 가정하십시오.
public static int test() {
/* final */ Object left = new Object();
Object right = new Object();
return left.hashCode() + right.hashCode();
}
이 코드를 그대로 컴파일하면 존재 했을 때와 정확히 동일한 바이트 코드가 생성 됩니다 final
( final Object left = new Object();
).
그러나 이것은 :
public static int test() {
/* final */ int left = 11;
int right = 12;
return left + right;
}
생산 :
0: bipush 11
2: istore_0
3: bipush 12
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: ireturn
final
현재 상태로 두면 다음이 생성됩니다.
0: bipush 12
2: istore_1
3: bipush 11
5: iload_1
6: iadd
7: ireturn
컴파일 타임 상수가있는 경우 코드는 거의 자명하지 않으며 피연산자 스택에 직접로드됩니다 (이전 예제와 같은 로컬 변수 배열에는 저장되지 않음 bipush 12; istore_0; iload_0
)-일종의 의미가 있습니다. 아무도 그것을 바꿀 수 없기 때문에.
반면에 두 번째 경우 컴파일러가 생성하지 않는 이유는 istore_0 ... iload_0
슬롯 0
을 사용하는 것과는 다릅니다 (변수 배열을 이런 식으로 축소 할 수는 있지만 일부 내부 세부 정보가 누락되어있을 수는 없습니다) 확실히 말해줘)
작은 최적화 방법을 고려할 때 이러한 최적화를보고 놀랐습니다 javac
. 우리는 항상 사용해야 final
합니까? 나는 심지어 JMH
테스트 를 작성하지 않을 것입니다 (처음에 원했던), 나는 diff가 ns
(가능한 경우 캡처 될 수 있는) 순서대로 있다고 확신합니다 . 이것이 문제가 될 수있는 유일한 방법은 크기 때문에 메소드를 인라인 할 수없는 경우입니다 (선언 final
하면 해당 크기가 몇 바이트 줄어 듭니다).
final
해결해야 할 두 가지가 더 있습니다. 첫째, 방법이 final
( JIT
관점에서) 있을 때 , 그러한 방법은 단형 적 이며- 이 방법이 가장 사랑받는 방법 입니다 JVM
.
그런 다음 final
모든 생성자에서 설정해야하는 인스턴스 변수가 있습니다. 여기에서 약간 터치하고로 정확히 지정된대로 올바르게 게시 된 참조를 보장하므로 중요 합니다 JLS
.
참고 : 자바 전문가가 아님
내 자바를 올바르게 기억한다면 최종 키워드를 사용하여 성능을 향상시킬 수있는 방법이 거의 없습니다. 나는 항상 "좋은 코드"-디자인과 가독성을 위해 존재한다는 것을 알고있었습니다.
나는 전문가는 아니지만 final
키워드를 덮어 쓰지 않고 변수를 남겨 두지 않으면 클래스 또는 메소드에 키워드를 추가해야한다고 가정합니다 . 그러한 것들을 최적화하는 방법이 있다면 컴파일러가 그렇게 할 것입니다.
실제로 일부 OpenGL 관련 코드를 테스트하는 동안 개인 필드에서 최종 수정자를 사용 하면 성능 이 저하 될 수 있습니다 . 테스트 한 수업의 시작은 다음과 같습니다.
public class ShaderInput {
private /* final */ float[] input;
private /* final */ int[] strides;
public ShaderInput()
{
this.input = new float[10];
this.strides = new int[] { 0, 4, 8 };
}
public ShaderInput x(int stride, float val)
{
input[strides[stride] + 0] = val;
return this;
}
// more stuff ...
그리고 이것은 ShaderInput 클래스 중 다양한 대안의 성능을 테스트하는 데 사용한 방법입니다.
public static void test4()
{
int arraySize = 10;
float[] fb = new float[arraySize];
for (int i = 0; i < arraySize; i++) {
fb[i] = random.nextFloat();
}
int times = 1000000000;
for (int i = 0; i < 10; ++i) {
floatVectorTest(times, fb);
arrayCopyTest(times, fb);
shaderInputTest(times, fb);
directFloatArrayTest(times, fb);
System.out.println();
System.gc();
}
}
3 번 반복 한 후 VM이 예열 된 후 최종 키워드 없이 지속적 으로이 수치 를 얻었습니다 .
Simple array copy took : 02.64
System.arrayCopy took : 03.20
ShaderInput took : 00.77
Unsafe float array took : 05.47
으로 최종 키워드 :
Simple array copy took : 02.66
System.arrayCopy took : 03.20
ShaderInput took : 02.59
Unsafe float array took : 06.24
ShaderInput 테스트 수치를 참고하십시오.
필드를 공개 또는 비공개로 설정했는지는 중요하지 않았습니다.
덧붙여서, 몇 가지 더 당황스러운 것들이 있습니다. ShaderInput 클래스는 final 키워드를 사용하더라도 다른 모든 변형을 능가합니다. 이것은 기본적으로 float 배열을 감싸는 클래스이며 다른 테스트 는 배열을 직접 조작합니다. 이것을 알아 내야합니다. ShaderInput의 유창한 인터페이스와 관련이있을 수 있습니다.
또한 System.arrayCopy는 실제로 작은 배열의 경우 for 루프에서 단순히 한 배열에서 다른 배열로 요소를 복사하는 것보다 다소 느립니다. 그리고 sun.misc.Unsafe (및 여기에 표시되지 않은 직접 java.nio.FloatBuffer)를 사용하면 아주 작습니다.
final로 선언 된 멤버는 최종 멤버가 아닌 멤버와 달리 프로그램에서 사용되지 않은 경우에도 가비지 콜렉터에서 관리하지 않으므로 메모리 관리가 잘못되어 성능 문제가 발생할 수 있기 때문에 프로그램 전체에서 사용할 수 있습니다.
final
키워드는 Java에서 5 가지 방식으로 사용될 수 있습니다.
- 수업은 최종
- 참조 변수는 최종입니다
- 지역 변수가 최종입니다
- 방법은 최종입니다
클래스는 최종적입니다 : 클래스는 최종적이란 우리가 확장 할 수 없거나 상속이 불가능하다는 것을 의미합니다.
비슷하게-객체는 최종적입니다 : 언젠가 우리는 객체의 내부 상태를 수정하지 않아서 객체를 최종 객체로 지정할 수 있습니다.
참조 변수가 최종적으로 만들어지면 다른 객체에 재 할당 할 수 없습니다. 그러나 필드가 최종이 아닌 한 객체의 내용을 변경할 수 있습니다
'Programing' 카테고리의 다른 글
클래스 간의 순환 종속성으로 인한 빌드 오류 해결 (0) | 2020.03.06 |
---|---|
공백이 아닌 변경 사항 만 추가 (0) | 2020.03.06 |
충돌하는 Git rebase 도중에 "그들의"변화를 얻는 방법? (0) | 2020.03.06 |
왜 참조 벡터를 만들 수 없습니까? (0) | 2020.03.06 |
Python에서 stdout을 파이핑 할 때 올바른 인코딩 설정 (0) | 2020.03.06 |